underpost 3.2.12 → 3.2.21
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 -0
- package/.github/workflows/npmpkg.ci.yml +9 -5
- package/CHANGELOG.md +114 -1
- package/CLI-HELP.md +973 -1130
- package/README.md +47 -41
- package/bin/build.js +88 -137
- package/bin/build.template.js +25 -179
- package/bin/deploy.js +4 -1
- package/bin/index.js +2 -2
- package/conf.js +11 -37
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +9 -14
- package/scripts/link-local-underpost-cli.sh +6 -0
- package/scripts/test-monitor.sh +86 -0
- package/src/cli/deploy.js +195 -274
- package/src/cli/env.js +1 -4
- package/src/cli/image.js +58 -4
- package/src/cli/index.js +39 -0
- package/src/cli/monitor.js +302 -6
- package/src/cli/release.js +26 -11
- package/src/cli/repository.js +98 -7
- package/src/cli/run.js +137 -69
- package/src/db/mongo/MongooseDB.js +2 -1
- package/src/index.js +1 -1
- package/src/runtime/wp/Dockerfile +3 -3
- package/src/server/catalog-underpost.js +61 -0
- package/src/server/catalog.js +77 -0
- package/src/server/conf.js +365 -56
- package/src/server/runtime-status.js +235 -0
- package/src/server/start.js +17 -8
- package/test/deploy-monitor.test.js +223 -0
- package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
- package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
package/src/cli/repository.js
CHANGED
|
@@ -133,10 +133,21 @@ class UnderpostRepository {
|
|
|
133
133
|
p: undefined,
|
|
134
134
|
bc: '',
|
|
135
135
|
isRemoteRepo: '',
|
|
136
|
+
hasChanges: false,
|
|
136
137
|
},
|
|
137
138
|
) {
|
|
138
139
|
if (!repoPath) repoPath = '.';
|
|
139
140
|
|
|
141
|
+
if (options.hasChanges) {
|
|
142
|
+
const status = shellExec(`cd ${repoPath} && git status --porcelain`, {
|
|
143
|
+
stdout: true,
|
|
144
|
+
silent: true,
|
|
145
|
+
disableLog: true,
|
|
146
|
+
}).trim();
|
|
147
|
+
process.stdout.write(status ? '1' : '');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
140
151
|
if (options.isRemoteRepo) {
|
|
141
152
|
const accessible = Underpost.repo.isRemoteRepo(options.isRemoteRepo);
|
|
142
153
|
console.log(accessible);
|
|
@@ -556,7 +567,7 @@ class UnderpostRepository {
|
|
|
556
567
|
// Handle sync-conf operation
|
|
557
568
|
if (options.syncConf) {
|
|
558
569
|
logger.info(`Syncing configuration for deploy ID: ${deployId}`);
|
|
559
|
-
shellExec(`node bin/build ${deployId} conf`);
|
|
570
|
+
shellExec(`node bin/build ${deployId} --conf`);
|
|
560
571
|
logger.info('Configuration synced successfully');
|
|
561
572
|
return resolve(true);
|
|
562
573
|
}
|
|
@@ -608,7 +619,8 @@ class UnderpostRepository {
|
|
|
608
619
|
const npmRoot = getNpmRootPath();
|
|
609
620
|
const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
|
|
610
621
|
const destFolder = `./${projectName}`;
|
|
611
|
-
|
|
622
|
+
const deployId = projectName.startsWith('dd-') ? projectName : `dd-${projectName}`;
|
|
623
|
+
logger.info('build app', { destFolder, deployId });
|
|
612
624
|
if (fs.existsSync(destFolder)) fs.removeSync(destFolder);
|
|
613
625
|
fs.mkdirSync(destFolder, { recursive: true });
|
|
614
626
|
if (!options.dev) {
|
|
@@ -621,8 +633,9 @@ class UnderpostRepository {
|
|
|
621
633
|
UnderpostRepository.API.initLocalRepo({ path: destFolder });
|
|
622
634
|
shellExec(`cd ${destFolder} && git add . && git commit -m "Base template implementation"`);
|
|
623
635
|
}
|
|
624
|
-
shellExec(`cd ${destFolder} &&
|
|
625
|
-
shellExec(`cd ${destFolder} &&
|
|
636
|
+
shellExec(`cd ${destFolder} && node bin new --deploy-id ${deployId} --default-conf`);
|
|
637
|
+
shellExec(`cd ${destFolder} && node bin client ${deployId}`);
|
|
638
|
+
shellExec(`cd ${destFolder} && DEPLOY_ID=${deployId} npm run dev`);
|
|
626
639
|
}
|
|
627
640
|
return resolve(true);
|
|
628
641
|
} catch (error) {
|
|
@@ -852,6 +865,7 @@ class UnderpostRepository {
|
|
|
852
865
|
}
|
|
853
866
|
}
|
|
854
867
|
await buildClient({
|
|
868
|
+
deployId: resolvedDeployId,
|
|
855
869
|
buildZip: options.buildZip || false,
|
|
856
870
|
split: options.split || '',
|
|
857
871
|
fullBuild: options.liteBuild ? false : true,
|
|
@@ -862,7 +876,12 @@ class UnderpostRepository {
|
|
|
862
876
|
logger.warn('Skip replica client build: replica folder not found', { replicaDeployId });
|
|
863
877
|
continue;
|
|
864
878
|
}
|
|
865
|
-
await Underpost.repo.client(replicaDeployId
|
|
879
|
+
await Underpost.repo.client(replicaDeployId, '', '', '', {
|
|
880
|
+
buildZip: options.buildZip || false,
|
|
881
|
+
split: options.split || '',
|
|
882
|
+
liteBuild: options.liteBuild || false,
|
|
883
|
+
iconsBuild: options.iconsBuild || false,
|
|
884
|
+
});
|
|
866
885
|
}
|
|
867
886
|
|
|
868
887
|
return resolve(true);
|
|
@@ -937,7 +956,7 @@ Prevent build private config repo.`,
|
|
|
937
956
|
deployVersion: packageJsonDeploy.version,
|
|
938
957
|
};
|
|
939
958
|
}
|
|
940
|
-
shellExec(`node bin/build ${deployId} conf`);
|
|
959
|
+
shellExec(`node bin/build ${deployId} --conf`);
|
|
941
960
|
return {
|
|
942
961
|
validVersion: true,
|
|
943
962
|
engineVersion: packageJsonEngine.version,
|
|
@@ -1374,8 +1393,9 @@ Prevent build private config repo.`,
|
|
|
1374
1393
|
const gitEmail = process.env.GITHUB_EMAIL || `development@underpost.net`;
|
|
1375
1394
|
|
|
1376
1395
|
if (!fs.existsSync(`${repoPath}/.git`)) {
|
|
1377
|
-
shellExec(`
|
|
1396
|
+
shellExec(`mkdir -p "${repoPath}" && git init "${repoPath}"`);
|
|
1378
1397
|
}
|
|
1398
|
+
|
|
1379
1399
|
shellExec(`cd "${repoPath}" && git config user.name '${gitUsername}'`);
|
|
1380
1400
|
shellExec(`cd "${repoPath}" && git config user.email '${gitEmail}'`);
|
|
1381
1401
|
shellExec(`cd "${repoPath}" && git config core.filemode false`);
|
|
@@ -1653,6 +1673,77 @@ Prevent build private config repo.`,
|
|
|
1653
1673
|
}
|
|
1654
1674
|
return fallback;
|
|
1655
1675
|
},
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* Performs a shallow sparse Git checkout of a single subdirectory from any
|
|
1679
|
+
* GitHub repository into a local target directory.
|
|
1680
|
+
*
|
|
1681
|
+
* Uses `--depth 1 --no-checkout` + `git sparse-checkout` so only the
|
|
1682
|
+
* requested path is fetched — no full clone of the remote repo.
|
|
1683
|
+
* Skips the clone entirely when `<targetDir>/<subPath>` already exists on
|
|
1684
|
+
* disk (idempotent).
|
|
1685
|
+
*
|
|
1686
|
+
* Requires `GITHUB_TOKEN` to be set in the environment for authenticated
|
|
1687
|
+
* access to private repositories.
|
|
1688
|
+
*
|
|
1689
|
+
* @param {string} subPath - The subdirectory path within the remote repo to
|
|
1690
|
+
* check out (e.g. `'conf/dd-prototype'`, `'src/api/payments'`).
|
|
1691
|
+
* @param {object} [options]
|
|
1692
|
+
* @param {string} [options.repoOwner='underpostnet'] - GitHub organisation or
|
|
1693
|
+
* user that owns the repository.
|
|
1694
|
+
* @param {string} [options.repoName='engine-private'] - Name of the
|
|
1695
|
+
* repository on GitHub.
|
|
1696
|
+
* @param {string} [options.targetDir='./engine-private'] - Local directory
|
|
1697
|
+
* where the repo will be cloned.
|
|
1698
|
+
* @returns {boolean} `true` when the checkout was performed, `false` when it
|
|
1699
|
+
* was skipped because the target path already existed.
|
|
1700
|
+
* @memberof UnderpostRepository
|
|
1701
|
+
*/
|
|
1702
|
+
sparseCheckoutDirectory(
|
|
1703
|
+
subPath,
|
|
1704
|
+
options = { repoOwner: 'underpostnet', repoName: 'engine-private', targetDir: './engine-private' },
|
|
1705
|
+
) {
|
|
1706
|
+
const { repoOwner = 'underpostnet', repoName = 'engine-private', targetDir = './engine-private' } = options;
|
|
1707
|
+
const localPath = `${targetDir}/${subPath}`;
|
|
1708
|
+
if (fs.existsSync(localPath)) {
|
|
1709
|
+
logger.info('[sparseCheckoutDirectory] path already present, skipping', localPath);
|
|
1710
|
+
return false;
|
|
1711
|
+
}
|
|
1712
|
+
const authUrl = `https://${process.env.GITHUB_TOKEN}@github.com/${repoOwner}/${repoName}.git`;
|
|
1713
|
+
shellExec(`git clone --depth 1 --no-checkout ${authUrl} ${targetDir}`, { disableLog: true });
|
|
1714
|
+
shellExec(`cd ${targetDir} && git sparse-checkout set ${subPath} && git checkout`, { disableLog: true });
|
|
1715
|
+
logger.info('[sparseCheckoutDirectory] sparse checkout complete', localPath);
|
|
1716
|
+
return true;
|
|
1717
|
+
},
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Ensures a deploy's public source repo (e.g. `engine-prototype`) is present
|
|
1721
|
+
* next to the engine and reset to a pristine HEAD, so catalog `sourceMoves`
|
|
1722
|
+
* can (re)pull custom sources even after a previous build moved them out of
|
|
1723
|
+
* the source tree.
|
|
1724
|
+
*
|
|
1725
|
+
* Clones `../<repoName>` when missing; otherwise restores a clean checkout
|
|
1726
|
+
* (`git checkout .` brings back any moved-out tracked files) and pulls latest.
|
|
1727
|
+
* Mirrors the sibling-repo handling used by `syncPrivateConf`.
|
|
1728
|
+
*
|
|
1729
|
+
* @param {string} repoName - Public source repo name (e.g. `engine-prototype`).
|
|
1730
|
+
* @returns {boolean} `true` when the repo is available on disk.
|
|
1731
|
+
* @memberof UnderpostRepository
|
|
1732
|
+
*/
|
|
1733
|
+
pullSourceRepo(repoName) {
|
|
1734
|
+
const username = process.env.GITHUB_USERNAME;
|
|
1735
|
+
if (!username || !repoName) return false;
|
|
1736
|
+
const repoPath = `../${repoName}`;
|
|
1737
|
+
const gitUri = `${username}/${repoName}`;
|
|
1738
|
+
if (!fs.existsSync(repoPath)) {
|
|
1739
|
+
shellExec(`cd .. && underpost clone ${gitUri}`, { silent: true });
|
|
1740
|
+
} else {
|
|
1741
|
+
shellExec(`cd ${repoPath} && git checkout . && git clean -f -d && underpost pull . ${gitUri}`, {
|
|
1742
|
+
silent: true,
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
return fs.existsSync(repoPath);
|
|
1746
|
+
},
|
|
1656
1747
|
};
|
|
1657
1748
|
}
|
|
1658
1749
|
|
package/src/cli/run.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getNpmRootPath,
|
|
16
16
|
isDeployRunnerContext,
|
|
17
17
|
loadConfServerJson,
|
|
18
|
+
loadReplicas,
|
|
18
19
|
writeEnv,
|
|
19
20
|
} from '../server/conf.js';
|
|
20
21
|
import { actionInitLog, loggerFactory } from '../server/logger.js';
|
|
@@ -264,6 +265,16 @@ class UnderpostRun {
|
|
|
264
265
|
logger.info(hostListenResult.renderHosts);
|
|
265
266
|
},
|
|
266
267
|
|
|
268
|
+
/**
|
|
269
|
+
* @method etc-hosts
|
|
270
|
+
* @description Modifies the `/etc/hosts` file to add entries for local access to services,
|
|
271
|
+
* based on the provided path input.
|
|
272
|
+
* @param {string} path - The input value, identifier, or path for the operation (used to specify the entries to add to /etc/hosts).
|
|
273
|
+
*/
|
|
274
|
+
'etc-hosts': (path = '', options = DEFAULT_OPTION) => {
|
|
275
|
+
etcHostFactory(path.split(','));
|
|
276
|
+
},
|
|
277
|
+
|
|
267
278
|
/**
|
|
268
279
|
* @method ipfs-expose
|
|
269
280
|
* @description Exposes IPFS Cluster services on specified ports for local access.
|
|
@@ -422,6 +433,7 @@ class UnderpostRun {
|
|
|
422
433
|
return;
|
|
423
434
|
}
|
|
424
435
|
shellExec(`${baseCommand} run pull`);
|
|
436
|
+
shellExec(`${baseCommand} run shared-dir`);
|
|
425
437
|
|
|
426
438
|
// Capture last N commit messages for propagation.
|
|
427
439
|
// When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
|
|
@@ -502,6 +514,7 @@ class UnderpostRun {
|
|
|
502
514
|
return;
|
|
503
515
|
}
|
|
504
516
|
shellExec(`${baseCommand} run pull`);
|
|
517
|
+
shellExec(`${baseCommand} run shared-dir`);
|
|
505
518
|
|
|
506
519
|
// Capture last N commit messages from the engine repo.
|
|
507
520
|
// When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
|
|
@@ -713,6 +726,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
713
726
|
let targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'green';
|
|
714
727
|
if (targetTraffic) versions = versions ? versions : targetTraffic;
|
|
715
728
|
|
|
729
|
+
const ignorePods =
|
|
730
|
+
isDeployRunnerContext(path, options) && targetTraffic
|
|
731
|
+
? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
|
|
732
|
+
: [];
|
|
733
|
+
|
|
716
734
|
const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
|
|
717
735
|
const cmdString = options.cmd
|
|
718
736
|
? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
|
|
@@ -743,7 +761,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
743
761
|
);
|
|
744
762
|
if (!targetTraffic)
|
|
745
763
|
targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
|
|
746
|
-
await Underpost.
|
|
764
|
+
await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
|
|
747
765
|
Underpost.deploy.switchTraffic(deployId, env, targetTraffic, replicas, options.namespace, options);
|
|
748
766
|
} else
|
|
749
767
|
logger.info('current traffic', Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }));
|
|
@@ -1154,7 +1172,7 @@ EOF
|
|
|
1154
1172
|
`,
|
|
1155
1173
|
{ disableLog: true },
|
|
1156
1174
|
);
|
|
1157
|
-
const { ready, readyPods } = await Underpost.
|
|
1175
|
+
const { ready, readyPods } = await Underpost.monitor.monitorReadyRunner(
|
|
1158
1176
|
_deployId,
|
|
1159
1177
|
env,
|
|
1160
1178
|
targetTraffic,
|
|
@@ -1436,8 +1454,8 @@ EOF`);
|
|
|
1436
1454
|
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
1437
1455
|
const currentImage = options.imageName
|
|
1438
1456
|
? options.imageName
|
|
1439
|
-
: Underpost.
|
|
1440
|
-
.
|
|
1457
|
+
: Underpost.image
|
|
1458
|
+
.getCurrentLoaded(options.nodeName ? options.nodeName : 'kind-worker', false)
|
|
1441
1459
|
.find((o) => o.IMAGE.match('underpost'));
|
|
1442
1460
|
const podName = options.podName || `underpost-dev-container`;
|
|
1443
1461
|
const volumeHostPath = options.claimName || '/home/dd';
|
|
@@ -1714,20 +1732,13 @@ EOF`);
|
|
|
1714
1732
|
const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
|
|
1715
1733
|
const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
|
|
1716
1734
|
const env = options.dev ? 'development' : 'production';
|
|
1717
|
-
const ignorePods = Underpost.
|
|
1735
|
+
const ignorePods = Underpost.kubectl
|
|
1718
1736
|
.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
|
|
1719
1737
|
.map((p) => p.NAME);
|
|
1720
1738
|
|
|
1721
1739
|
shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${options.namespace}`);
|
|
1722
1740
|
|
|
1723
|
-
await Underpost.
|
|
1724
|
-
deployId,
|
|
1725
|
-
env,
|
|
1726
|
-
targetTraffic,
|
|
1727
|
-
ignorePods,
|
|
1728
|
-
options.namespace,
|
|
1729
|
-
'underpost',
|
|
1730
|
-
);
|
|
1741
|
+
await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
|
|
1731
1742
|
|
|
1732
1743
|
Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, options.namespace, options);
|
|
1733
1744
|
},
|
|
@@ -1819,7 +1830,7 @@ EOF`);
|
|
|
1819
1830
|
}`;
|
|
1820
1831
|
shellExec(cmd, { async: true });
|
|
1821
1832
|
}
|
|
1822
|
-
await awaitDeployMonitor();
|
|
1833
|
+
if ((await awaitDeployMonitor()) !== true) return;
|
|
1823
1834
|
{
|
|
1824
1835
|
const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
|
|
1825
1836
|
|
|
@@ -1827,7 +1838,7 @@ EOF`);
|
|
|
1827
1838
|
async: true,
|
|
1828
1839
|
});
|
|
1829
1840
|
}
|
|
1830
|
-
await awaitDeployMonitor();
|
|
1841
|
+
if ((await awaitDeployMonitor()) !== true) return;
|
|
1831
1842
|
shellExec(
|
|
1832
1843
|
`NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
|
|
1833
1844
|
);
|
|
@@ -2504,7 +2515,8 @@ EOF`;
|
|
|
2504
2515
|
/**
|
|
2505
2516
|
* @method push-bundle
|
|
2506
2517
|
* @description Builds the client zip for the specified deployment, splits it into parts, and uploads to file storage.
|
|
2507
|
-
* Steps: set env, build+split zip,
|
|
2518
|
+
* Steps: set env, build+split zip, upload only the zip parts belonging to the deploy-id's hosts (from conf.server.json).
|
|
2519
|
+
* Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
|
|
2508
2520
|
* @param {string} path - Optional `fsPath.splitOption` string.
|
|
2509
2521
|
* Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
|
|
2510
2522
|
* @param {Object} options - The default underpost runner options for customizing workflow.
|
|
@@ -2513,7 +2525,7 @@ EOF`;
|
|
|
2513
2525
|
* @memberof UnderpostRun
|
|
2514
2526
|
*/
|
|
2515
2527
|
'push-bundle': (path = '', options = DEFAULT_OPTION) => {
|
|
2516
|
-
const baseCommand = options.dev ? 'node bin' : 'underpost';
|
|
2528
|
+
const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
|
|
2517
2529
|
const env = options.dev ? 'development' : 'production';
|
|
2518
2530
|
const deployId = options.deployId || 'dd-default';
|
|
2519
2531
|
const pathParts = (path || '').split('.');
|
|
@@ -2537,11 +2549,54 @@ EOF`;
|
|
|
2537
2549
|
}
|
|
2538
2550
|
}
|
|
2539
2551
|
|
|
2552
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
2553
|
+
const confServer = fs.existsSync(confServerPath)
|
|
2554
|
+
? loadReplicas(deployId, loadConfServerJson(confServerPath))
|
|
2555
|
+
: {};
|
|
2556
|
+
const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
|
|
2557
|
+
|
|
2540
2558
|
shellExec(`${baseCommand} env ${deployId} ${env}`);
|
|
2541
2559
|
shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2560
|
+
|
|
2561
|
+
const pushBundleFiles = (host, routePath) => {
|
|
2562
|
+
const buildId = `${host}-${routePath.replaceAll('/', '')}`;
|
|
2563
|
+
const buildDir = `./${fsPath}`;
|
|
2564
|
+
if (!fs.existsSync(buildDir)) return;
|
|
2565
|
+
const partFiles = fs
|
|
2566
|
+
.readdirSync(buildDir)
|
|
2567
|
+
.filter(
|
|
2568
|
+
(name) =>
|
|
2569
|
+
name.startsWith(`${buildId}.zip.part`) ||
|
|
2570
|
+
name.startsWith(`${buildId}.zip-part`) ||
|
|
2571
|
+
name === `${buildId}.zip`,
|
|
2572
|
+
)
|
|
2573
|
+
.map((name) => `${fsPath}/${name}`);
|
|
2574
|
+
if (partFiles.length === 0) {
|
|
2575
|
+
logger.warn(`push-bundle: no bundle files found for '${host}${routePath}'`, { buildId });
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
for (const partFile of partFiles) {
|
|
2579
|
+
shellExec(
|
|
2580
|
+
`${baseCommand} fs ${partFile} --deploy-id ${deployId} --storage-file-path ${storageFilePath} --force`,
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
|
|
2585
|
+
for (const host of Object.keys(confServer)) {
|
|
2586
|
+
for (const routePath of Object.keys(confServer[host])) {
|
|
2587
|
+
const routeConf = confServer[host][routePath] || {};
|
|
2588
|
+
if (routeConf.redirect || routeConf.disabledRebuild) continue;
|
|
2589
|
+
if (routeConf.singleReplica) {
|
|
2590
|
+
if (routeConf.replicas) {
|
|
2591
|
+
for (const replica of routeConf.replicas) {
|
|
2592
|
+
pushBundleFiles(host, replica);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
continue;
|
|
2596
|
+
}
|
|
2597
|
+
pushBundleFiles(host, routePath);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2545
2600
|
},
|
|
2546
2601
|
|
|
2547
2602
|
/**
|
|
@@ -2558,11 +2613,13 @@ EOF`;
|
|
|
2558
2613
|
* @memberof UnderpostRun
|
|
2559
2614
|
*/
|
|
2560
2615
|
'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
|
|
2561
|
-
const baseCommand = options.dev ? 'node bin' : 'underpost';
|
|
2616
|
+
const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
|
|
2562
2617
|
const env = options.dev ? 'development' : 'production';
|
|
2563
2618
|
const deployId = options.deployId || 'dd-default';
|
|
2564
2619
|
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
2565
|
-
const confServer = fs.existsSync(confServerPath)
|
|
2620
|
+
const confServer = fs.existsSync(confServerPath)
|
|
2621
|
+
? loadReplicas(deployId, loadConfServerJson(confServerPath))
|
|
2622
|
+
: {};
|
|
2566
2623
|
const hostsArg = path
|
|
2567
2624
|
? path
|
|
2568
2625
|
.split(',')
|
|
@@ -2581,64 +2638,75 @@ EOF`;
|
|
|
2581
2638
|
`${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
|
|
2582
2639
|
);
|
|
2583
2640
|
|
|
2641
|
+
const pullBundleRoute = (host, routePath) => {
|
|
2642
|
+
const buildId = `${host}-${routePath.replaceAll('/', '')}`;
|
|
2643
|
+
const zipPath = `build/${buildId}.zip`;
|
|
2644
|
+
const buildDir = './build';
|
|
2645
|
+
const hasZip = fs.existsSync(zipPath);
|
|
2646
|
+
const hasParts =
|
|
2647
|
+
fs.existsSync(buildDir) &&
|
|
2648
|
+
fs
|
|
2649
|
+
.readdirSync(buildDir)
|
|
2650
|
+
.some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
|
|
2651
|
+
|
|
2652
|
+
if (!hasZip && !hasParts) {
|
|
2653
|
+
logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
|
|
2658
|
+
shellExec(`${baseCommand} client --unzip ${zipPath}`);
|
|
2659
|
+
shellExec(`sudo rm -rf ${zipPath}`);
|
|
2660
|
+
|
|
2661
|
+
if (fs.existsSync(buildDir)) {
|
|
2662
|
+
fs.readdirSync(buildDir)
|
|
2663
|
+
.filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
|
|
2664
|
+
.forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
const extractedDir = `build/${buildId.replace(/-$/, '')}`;
|
|
2668
|
+
if (!fs.existsSync(extractedDir)) {
|
|
2669
|
+
logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
|
|
2674
|
+
if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
|
|
2675
|
+
if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
|
|
2676
|
+
fs.copySync(`${extractedDir}`, `${publicDestPath}`);
|
|
2677
|
+
};
|
|
2678
|
+
|
|
2584
2679
|
for (const host of hostsArg) {
|
|
2585
|
-
// Gather all routes for this host; fall back to root '/' when host is not in confServer
|
|
2586
|
-
// (e.g. when hosts were provided explicitly via the path argument).
|
|
2587
2680
|
const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
|
|
2588
2681
|
|
|
2589
2682
|
for (const routePath of routePaths) {
|
|
2590
2683
|
const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
|
|
2591
|
-
|
|
2592
|
-
if (routeConf.singleReplica
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
const zipPath = `build/${buildId}.zip`;
|
|
2599
|
-
const buildDir = './build';
|
|
2600
|
-
const hasZip = fs.existsSync(zipPath);
|
|
2601
|
-
const hasParts =
|
|
2602
|
-
fs.existsSync(buildDir) &&
|
|
2603
|
-
fs
|
|
2604
|
-
.readdirSync(buildDir)
|
|
2605
|
-
.some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
|
|
2606
|
-
|
|
2607
|
-
if (!hasZip && !hasParts) {
|
|
2608
|
-
logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
|
|
2609
|
-
continue;
|
|
2610
|
-
}
|
|
2611
|
-
|
|
2612
|
-
if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
|
|
2613
|
-
shellExec(`${baseCommand} client --unzip ${zipPath}`);
|
|
2614
|
-
shellExec(`sudo rm -rf ${zipPath}`);
|
|
2615
|
-
|
|
2616
|
-
// Clean up downloaded part wrapper zips left by --omit-unzip pull
|
|
2617
|
-
if (fs.existsSync(buildDir)) {
|
|
2618
|
-
fs.readdirSync(buildDir)
|
|
2619
|
-
.filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
|
|
2620
|
-
.forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
// unzipClientBuild extracts to buildId with trailing '-' stripped
|
|
2624
|
-
// e.g. "build/underpost.net-" → "build/underpost.net"
|
|
2625
|
-
// e.g. "build/app.net-admin" → "build/app.net-admin" (no trailing dash, no change)
|
|
2626
|
-
const extractedDir = `build/${buildId.replace(/-$/, '')}`;
|
|
2627
|
-
if (!fs.existsSync(extractedDir)) {
|
|
2628
|
-
logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
|
|
2684
|
+
if (routeConf.redirect || routeConf.disabledRebuild) continue;
|
|
2685
|
+
if (routeConf.singleReplica) {
|
|
2686
|
+
if (routeConf.replicas) {
|
|
2687
|
+
for (const replica of routeConf.replicas) {
|
|
2688
|
+
pullBundleRoute(host, replica);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2629
2691
|
continue;
|
|
2630
2692
|
}
|
|
2631
|
-
|
|
2632
|
-
// Destination mirrors the public directory layout used by the server
|
|
2633
|
-
const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
|
|
2634
|
-
if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
|
|
2635
|
-
// Ensure parent directory exists for sub-paths
|
|
2636
|
-
if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
|
|
2637
|
-
shellExec(`sudo mv ${extractedDir} ${publicDestPath}`);
|
|
2693
|
+
pullBundleRoute(host, routePath);
|
|
2638
2694
|
}
|
|
2639
2695
|
}
|
|
2640
2696
|
},
|
|
2641
2697
|
|
|
2698
|
+
/**
|
|
2699
|
+
* @method build-cluster-deployment-manifests
|
|
2700
|
+
* @description Builds deployment manifests for both production and development environments using `node bin deploy --build-manifest`, syncing them, and setting replicas to 1 for the `dd` deployment.
|
|
2701
|
+
* @param {string} path - Unused.
|
|
2702
|
+
* @param {Object} options - The default underpost runner options for customizing workflow.
|
|
2703
|
+
* @memberof UnderpostRun
|
|
2704
|
+
*/
|
|
2705
|
+
'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
|
|
2706
|
+
shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
|
|
2707
|
+
shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
|
|
2708
|
+
},
|
|
2709
|
+
|
|
2642
2710
|
/**
|
|
2643
2711
|
* @method monitor-ui
|
|
2644
2712
|
* @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
|
|
@@ -86,7 +86,8 @@ class MongooseDBService {
|
|
|
86
86
|
|
|
87
87
|
const user = config.user || process.env.DB_USER || '';
|
|
88
88
|
const password = config.password || process.env.DB_PASSWORD || '';
|
|
89
|
-
const
|
|
89
|
+
const hasExplicitReplicaSet = !!(config.replicaSet || process.env.DB_REPLICA_SET);
|
|
90
|
+
const directConnection = hosts.length === 1 && !hasExplicitReplicaSet;
|
|
90
91
|
const replicaSet = directConnection
|
|
91
92
|
? ''
|
|
92
93
|
: config.replicaSet || process.env.DB_REPLICA_SET || MONGODB_DEFAULT_REPLICA_SET;
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@ FROM rockylinux:9
|
|
|
3
3
|
# System packages
|
|
4
4
|
RUN dnf -y update && \
|
|
5
5
|
dnf -y install epel-release && \
|
|
6
|
-
dnf -y install --allowerasing \
|
|
6
|
+
dnf -y install --allowerasing --nobest \
|
|
7
7
|
bzip2 \
|
|
8
8
|
sudo \
|
|
9
9
|
curl \
|
|
@@ -20,8 +20,8 @@ RUN dnf -y update && \
|
|
|
20
20
|
perl && \
|
|
21
21
|
dnf clean all
|
|
22
22
|
|
|
23
|
-
# Download and install XAMPP (PHP 8.2)
|
|
24
|
-
RUN curl -L -o /tmp/xampp-linux-installer.run "https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/8.2.12/xampp-linux-x64-8.2.12-0-installer.run" && \
|
|
23
|
+
# Download and install XAMPP (PHP 8.2) with retry logic for flaky SourceForge transfers
|
|
24
|
+
RUN curl -L --retry 5 --retry-delay 10 --retry-max-time 180 -o /tmp/xampp-linux-installer.run "https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/8.2.12/xampp-linux-x64-8.2.12-0-installer.run" && \
|
|
25
25
|
chmod +x /tmp/xampp-linux-installer.run && \
|
|
26
26
|
bash -c "/tmp/xampp-linux-installer.run --mode unattended" && \
|
|
27
27
|
ln -sf /opt/lampp/lampp /usr/bin/lampp
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Underpost platform content catalog — the base `pwa-microservices-template`.
|
|
3
|
+
*
|
|
4
|
+
* @module src/server/catalog-underpost.js
|
|
5
|
+
* @namespace UnderpostCatalog
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Workflow + service files re-added to the template after the engine-only strip.
|
|
10
|
+
* @constant {string[]}
|
|
11
|
+
* @memberof UnderpostCatalog
|
|
12
|
+
*/
|
|
13
|
+
const TEMPLATE_RESTORE_PATHS = [
|
|
14
|
+
`./src/server/catalog-underpost.js`,
|
|
15
|
+
`./.github/workflows/pwa-microservices-template-page.cd.yml`,
|
|
16
|
+
`./.github/workflows/pwa-microservices-template-test.ci.yml`,
|
|
17
|
+
`./.github/workflows/npmpkg.ci.yml`,
|
|
18
|
+
`./.github/workflows/ghpkg.ci.yml`,
|
|
19
|
+
`./.github/workflows/gitlab.ci.yml`,
|
|
20
|
+
`./.github/workflows/publish.ci.yml`,
|
|
21
|
+
`./.github/workflows/release.cd.yml`,
|
|
22
|
+
`./src/client/services/user/guest.service.js`,
|
|
23
|
+
'./src/api/user/guest.service.js',
|
|
24
|
+
'./src/ws/IoInterface.js',
|
|
25
|
+
'./src/ws/IoServer.js',
|
|
26
|
+
'./manifests/deployment/dd-default-development',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* npm keywords for the standalone Underpost platform / template package.
|
|
31
|
+
* @constant {string[]}
|
|
32
|
+
* @memberof UnderpostCatalog
|
|
33
|
+
*/
|
|
34
|
+
const TEMPLATE_KEYWORDS = [
|
|
35
|
+
'underpost',
|
|
36
|
+
'underpost-platform',
|
|
37
|
+
'cli',
|
|
38
|
+
'toolchain',
|
|
39
|
+
'ci-cd',
|
|
40
|
+
'devops',
|
|
41
|
+
'kubernetes',
|
|
42
|
+
'k3s',
|
|
43
|
+
'kubeadm',
|
|
44
|
+
'lxd',
|
|
45
|
+
'baremetal',
|
|
46
|
+
'container-orchestration',
|
|
47
|
+
'image-management',
|
|
48
|
+
'pwa',
|
|
49
|
+
'workbox',
|
|
50
|
+
'microservices',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* npm description for the standalone Underpost platform / template package.
|
|
55
|
+
* @constant {string}
|
|
56
|
+
* @memberof UnderpostCatalog
|
|
57
|
+
*/
|
|
58
|
+
const TEMPLATE_DESCRIPTION =
|
|
59
|
+
'Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orchestration. Extensible via downstream CLIs.';
|
|
60
|
+
|
|
61
|
+
export { TEMPLATE_RESTORE_PATHS, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION };
|