underpost 3.2.0 → 3.2.2
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 +6 -0
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -1
- package/.github/workflows/release.cd.yml +10 -5
- package/CHANGELOG.md +73 -1
- package/CLI-HELP.md +5 -4
- package/Dockerfile +4 -2
- package/README.md +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +18 -26
- package/package.json +2 -2
- package/src/cli/db.js +687 -620
- package/src/cli/deploy.js +47 -28
- package/src/cli/env.js +18 -0
- package/src/cli/fs.js +3 -1
- package/src/cli/index.js +3 -1
- package/src/cli/repository.js +143 -0
- package/src/cli/run.js +1 -1
- package/src/cli/secrets.js +73 -0
- package/src/client/components/core/DropDown.js +13 -5
- package/src/index.js +1 -1
- package/src/runtime/express/Dockerfile +4 -0
- package/src/runtime/lampp/Dockerfile +4 -0
- package/src/runtime/lampp/Lampp.js +23 -1
- package/src/runtime/wp/Dockerfile +4 -0
- package/src/runtime/wp/Wp.js +148 -6
- package/src/server/backup.js +57 -41
- package/src/server/cron.js +23 -18
- package/src/server/start.js +2 -7
package/src/cli/deploy.js
CHANGED
|
@@ -132,22 +132,16 @@ class UnderpostDeploy {
|
|
|
132
132
|
cmd = [
|
|
133
133
|
`npm install -g npm@11.2.0`,
|
|
134
134
|
`npm install -g underpost`,
|
|
135
|
-
`underpost secret underpost --create-from-
|
|
135
|
+
`underpost secret underpost --create-from-env`,
|
|
136
136
|
`underpost start --build --run ${deployId} ${env}`,
|
|
137
137
|
];
|
|
138
138
|
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|
139
|
-
if (!volumes)
|
|
140
|
-
volumes = [
|
|
141
|
-
{
|
|
142
|
-
volumeMountPath: '/etc/config',
|
|
143
|
-
volumeName: 'config-volume',
|
|
144
|
-
configMap: 'underpost-config',
|
|
145
|
-
},
|
|
146
|
-
];
|
|
139
|
+
if (!volumes) volumes = [];
|
|
147
140
|
const confVolume = fs.existsSync(`./engine-private/conf/${deployId}/conf.volume.json`)
|
|
148
141
|
? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
|
|
149
142
|
: [];
|
|
150
143
|
volumes = volumes.concat(confVolume);
|
|
144
|
+
const containerImage = image ? image : `localhost/rockylinux9-underpost:v${packageJson.version}`;
|
|
151
145
|
return `apiVersion: apps/v1
|
|
152
146
|
kind: Deployment
|
|
153
147
|
metadata:
|
|
@@ -169,7 +163,10 @@ spec:
|
|
|
169
163
|
spec:
|
|
170
164
|
containers:
|
|
171
165
|
- name: ${deployId}-${env}-${suffix}
|
|
172
|
-
image: ${
|
|
166
|
+
image: ${containerImage}
|
|
167
|
+
envFrom:
|
|
168
|
+
- secretRef:
|
|
169
|
+
name: underpost-config
|
|
173
170
|
${
|
|
174
171
|
resources
|
|
175
172
|
? ` resources:
|
|
@@ -185,13 +182,17 @@ ${
|
|
|
185
182
|
- /bin/sh
|
|
186
183
|
- -c
|
|
187
184
|
- >
|
|
188
|
-
${cmd.join(
|
|
185
|
+
${cmd.join(' &&\n ')}
|
|
189
186
|
|
|
190
|
-
${
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
187
|
+
${
|
|
188
|
+
volumes.length > 0
|
|
189
|
+
? Underpost.deploy
|
|
190
|
+
.volumeFactory(volumes.map((v) => ((v.version = `${deployId}-${env}-${suffix}`), v)))
|
|
191
|
+
.render.split(`\n`)
|
|
192
|
+
.map((l) => ' ' + l)
|
|
193
|
+
.join(`\n`)
|
|
194
|
+
: ''
|
|
195
|
+
}
|
|
195
196
|
---
|
|
196
197
|
apiVersion: v1
|
|
197
198
|
kind: Service
|
|
@@ -793,15 +794,16 @@ EOF`);
|
|
|
793
794
|
};
|
|
794
795
|
},
|
|
795
796
|
/**
|
|
796
|
-
* Creates a
|
|
797
|
-
*
|
|
798
|
-
* @param {string}
|
|
797
|
+
* Creates a Kubernetes Secret for a deployment (replaces configMap for secret data).
|
|
798
|
+
* Secrets are mounted as tmpfs (never written to node disk) and support RBAC restrictions.
|
|
799
|
+
* @param {string} env - Environment for which the secret is being created.
|
|
800
|
+
* @param {string} [namespace='default'] - Kubernetes namespace for the secret.
|
|
799
801
|
* @memberof UnderpostDeploy
|
|
800
802
|
*/
|
|
801
803
|
configMap(env, namespace = 'default') {
|
|
802
|
-
shellExec(`kubectl delete
|
|
804
|
+
shellExec(`kubectl delete secret underpost-config -n ${namespace} --ignore-not-found`);
|
|
803
805
|
shellExec(
|
|
804
|
-
`kubectl create
|
|
806
|
+
`kubectl create secret generic underpost-config --from-env-file=/home/dd/engine/engine-private/conf/dd-cron/.env.${env} --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
|
|
805
807
|
);
|
|
806
808
|
},
|
|
807
809
|
/**
|
|
@@ -920,6 +922,8 @@ EOF
|
|
|
920
922
|
* @param {string} volume.volumeType - Type of the volume (e.g. 'Directory').
|
|
921
923
|
* @param {string|null} volume.claimName - Name of the persistent volume claim (if applicable).
|
|
922
924
|
* @param {string|null} volume.configMap - Name of the config map (if applicable).
|
|
925
|
+
* @param {string|null} volume.secret - Name of the Kubernetes Secret (if applicable). Mounts as readOnly.
|
|
926
|
+
* @param {boolean} [volume.emptyDir=false] - If true, uses an emptyDir volume (writable tmpfs).
|
|
923
927
|
* @returns {object} - Object containing the rendered volume mounts and volumes.
|
|
924
928
|
* @memberof UnderpostDeploy
|
|
925
929
|
*/
|
|
@@ -941,7 +945,17 @@ EOF
|
|
|
941
945
|
let _volumes = `
|
|
942
946
|
volumes:`;
|
|
943
947
|
volumes.map((volumeData) => {
|
|
944
|
-
let {
|
|
948
|
+
let {
|
|
949
|
+
volumeName,
|
|
950
|
+
volumeMountPath,
|
|
951
|
+
volumeHostPath,
|
|
952
|
+
volumeType,
|
|
953
|
+
claimName,
|
|
954
|
+
configMap,
|
|
955
|
+
secret,
|
|
956
|
+
emptyDir,
|
|
957
|
+
version,
|
|
958
|
+
} = volumeData;
|
|
945
959
|
if (version) {
|
|
946
960
|
volumeName = `${volumeName}-${version}`;
|
|
947
961
|
claimName = claimName ? `${claimName}-${version}` : null;
|
|
@@ -949,18 +963,23 @@ EOF
|
|
|
949
963
|
_volumeMounts += `
|
|
950
964
|
- name: ${volumeName}
|
|
951
965
|
mountPath: ${volumeMountPath}
|
|
952
|
-
`;
|
|
966
|
+
${secret ? ` readOnly: true\n` : ''}`;
|
|
953
967
|
|
|
954
968
|
_volumes += `
|
|
955
969
|
- name: ${volumeName}
|
|
956
970
|
${
|
|
957
|
-
|
|
958
|
-
? `
|
|
971
|
+
emptyDir
|
|
972
|
+
? ` emptyDir: {}`
|
|
973
|
+
: secret
|
|
974
|
+
? ` secret:
|
|
975
|
+
secretName: ${secret}`
|
|
976
|
+
: configMap
|
|
977
|
+
? ` configMap:
|
|
959
978
|
name: ${configMap}`
|
|
960
|
-
|
|
961
|
-
|
|
979
|
+
: claimName
|
|
980
|
+
? ` persistentVolumeClaim:
|
|
962
981
|
claimName: ${claimName}`
|
|
963
|
-
|
|
982
|
+
: ` hostPath:
|
|
964
983
|
path: ${volumeHostPath}
|
|
965
984
|
type: ${volumeType}
|
|
966
985
|
`
|
package/src/cli/env.js
CHANGED
|
@@ -12,6 +12,19 @@ import { pbcopy } from '../server/process.js';
|
|
|
12
12
|
|
|
13
13
|
const logger = loggerFactory(import.meta);
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Guards an env file path against stale directory artifacts.
|
|
17
|
+
* Removes the path if it exists as a directory (e.g. `.env/` created by a previous EISDIR bug).
|
|
18
|
+
* @param {string} envPath - The path to the environment file.
|
|
19
|
+
* @memberof UnderpostEnv
|
|
20
|
+
*/
|
|
21
|
+
const guardEnvPath = (envPath) => {
|
|
22
|
+
if (fs.existsSync(envPath) && !fs.statSync(envPath).isFile()) {
|
|
23
|
+
logger.warn(`Removing stale directory at env path: ${envPath}`);
|
|
24
|
+
fs.removeSync(envPath);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
15
28
|
/**
|
|
16
29
|
* @class UnderpostRootEnv
|
|
17
30
|
* @description Manages the environment variables of the underpost root.
|
|
@@ -31,6 +44,7 @@ class UnderpostRootEnv {
|
|
|
31
44
|
*/
|
|
32
45
|
set(key, value, options = { deployId: '', build: false }) {
|
|
33
46
|
const _set = (envPath, key, value) => {
|
|
47
|
+
guardEnvPath(envPath);
|
|
34
48
|
let env = {};
|
|
35
49
|
if (fs.existsSync(envPath)) env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
36
50
|
env[key] = value;
|
|
@@ -61,6 +75,7 @@ class UnderpostRootEnv {
|
|
|
61
75
|
delete(key) {
|
|
62
76
|
const exeRootPath = `${getNpmRootPath()}/underpost`;
|
|
63
77
|
const envPath = `${exeRootPath}/.env`;
|
|
78
|
+
guardEnvPath(envPath);
|
|
64
79
|
let env = {};
|
|
65
80
|
if (fs.existsSync(envPath)) env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
66
81
|
delete env[key];
|
|
@@ -102,6 +117,7 @@ class UnderpostRootEnv {
|
|
|
102
117
|
list(key, value, options = {}) {
|
|
103
118
|
const exeRootPath = `${getNpmRootPath()}/underpost`;
|
|
104
119
|
const envPath = `${exeRootPath}/.env`;
|
|
120
|
+
guardEnvPath(envPath);
|
|
105
121
|
if (!fs.existsSync(envPath)) {
|
|
106
122
|
logger.warn(`Empty environment variables`);
|
|
107
123
|
return {};
|
|
@@ -141,3 +157,5 @@ class UnderpostRootEnv {
|
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
export default UnderpostRootEnv;
|
|
160
|
+
|
|
161
|
+
export { guardEnvPath };
|
package/src/cli/fs.js
CHANGED
|
@@ -101,12 +101,14 @@ class UnderpostFileStorage {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
if (options.pull === true) {
|
|
104
|
+
let pullSkipCount = 0;
|
|
104
105
|
for (const _path of Object.keys(storage)) {
|
|
105
106
|
if (!fs.existsSync(_path) || options.force === true) {
|
|
106
107
|
if (options.force === true && fs.existsSync(_path)) fs.removeSync(_path);
|
|
107
108
|
await Underpost.fs.pull(_path, options);
|
|
108
|
-
} else
|
|
109
|
+
} else pullSkipCount++;
|
|
109
110
|
}
|
|
111
|
+
if (pullSkipCount > 0) logger.warn(`Pull skipped ${pullSkipCount} files that already exist`);
|
|
110
112
|
Underpost.repo.initLocalRepo({ path });
|
|
111
113
|
shellExec(`cd ${path} && git add . && git commit -m "Base pull state"`);
|
|
112
114
|
} else {
|
package/src/cli/index.js
CHANGED
|
@@ -337,10 +337,12 @@ program
|
|
|
337
337
|
.argument('<platform>', `The secret management platform. Options: ${Object.keys(Underpost.secret).join(', ')}.`)
|
|
338
338
|
.option('--init', 'Initializes the secrets platform environment.')
|
|
339
339
|
.option('--create-from-file <path-env-file>', 'Creates secrets from a specified environment file.')
|
|
340
|
+
.option('--create-from-env', 'Creates secrets from container environment variables (envFrom: secretRef).')
|
|
340
341
|
.option('--list', 'Lists all available secrets for the platform.')
|
|
341
342
|
.description(`Manages secrets for various platforms.`)
|
|
342
343
|
.action((...args) => {
|
|
343
344
|
if (args[1].createFromFile) return Underpost.secret[args[0]].createFromEnvFile(args[1].createFromFile);
|
|
345
|
+
if (args[1].createFromEnv) return Underpost.secret[args[0]].createFromContainerEnv();
|
|
344
346
|
if (args[1].list) return Underpost.secret[args[0]].list();
|
|
345
347
|
if (args[1].init) return Underpost.secret[args[0]].init();
|
|
346
348
|
});
|
|
@@ -423,6 +425,7 @@ program
|
|
|
423
425
|
.option('--kubeadm', 'Enables the kubeadm context for database operations.')
|
|
424
426
|
.option('--kind', 'Enables the kind context for database operations.')
|
|
425
427
|
.option('--k3s', 'Enables the k3s context for database operations.')
|
|
428
|
+
.option('--repo-backup', 'Backs up repositories (git commit+push) inside deployment pods via kubectl exec.')
|
|
426
429
|
.description(
|
|
427
430
|
'Manages database operations with support for MariaDB and MongoDB, including import/export, multi-pod targeting, and Git integration.',
|
|
428
431
|
)
|
|
@@ -468,7 +471,6 @@ program
|
|
|
468
471
|
'--create-job-now',
|
|
469
472
|
'After applying manifests, immediately create a Job from each CronJob (requires --apply).',
|
|
470
473
|
)
|
|
471
|
-
.option('--ssh', 'Execute backup commands via SSH on the remote node instead of locally.')
|
|
472
474
|
.description('Manages cron jobs: execute jobs directly or generate and apply K8s CronJob manifests.')
|
|
473
475
|
.action(Underpost.cron.callback);
|
|
474
476
|
|
package/src/cli/repository.js
CHANGED
|
@@ -1423,6 +1423,149 @@ Prevent build private config repo.`,
|
|
|
1423
1423
|
return false;
|
|
1424
1424
|
}
|
|
1425
1425
|
},
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Runtime-type → in-pod site-root directory resolver.
|
|
1429
|
+
* Maps each known runtime to the filesystem path where its repository lives inside the container.
|
|
1430
|
+
* @param {string} runtime - The runtime identifier (e.g. 'wp').
|
|
1431
|
+
* @param {string} host - The virtual-host name.
|
|
1432
|
+
* @returns {string|null} Absolute path inside the pod, or null if the runtime has no known mapping.
|
|
1433
|
+
* @memberof UnderpostRepository
|
|
1434
|
+
*/
|
|
1435
|
+
runtimeSiteRoot(runtime, host) {
|
|
1436
|
+
const runtimePaths = {
|
|
1437
|
+
wp: `/opt/lampp/htdocs/wp/${host}`,
|
|
1438
|
+
};
|
|
1439
|
+
return runtimePaths[runtime] || null;
|
|
1440
|
+
},
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Backs up all repositories defined in a deployment's conf.server.json by executing
|
|
1444
|
+
* git commit+push inside the running deployment pod via `kubectl exec`.
|
|
1445
|
+
*
|
|
1446
|
+
* Scans every `server[host][path]` entry for a `repository` field. For each match
|
|
1447
|
+
* the runtime-specific site root is resolved and a git backup script is executed
|
|
1448
|
+
* inside the pod. GITHUB_TOKEN and GITHUB_USERNAME are injected as ephemeral
|
|
1449
|
+
* environment variables in the exec command — never persisted to the pod filesystem.
|
|
1450
|
+
*
|
|
1451
|
+
* @param {object} opts
|
|
1452
|
+
* @param {string} opts.deployId - Deployment ID (used to read conf.server.json and find pods).
|
|
1453
|
+
* @param {string} [opts.namespace='default'] - Kubernetes namespace.
|
|
1454
|
+
* @param {string} [opts.env='production'] - Deployment environment.
|
|
1455
|
+
* @returns {void}
|
|
1456
|
+
* @memberof UnderpostRepository
|
|
1457
|
+
*/
|
|
1458
|
+
backupPodRepositories({ deployId, namespace = 'default', env = 'production' }) {
|
|
1459
|
+
const confServer = readConfJson(deployId, 'server', { resolve: true });
|
|
1460
|
+
const githubToken = process.env.GITHUB_TOKEN || '';
|
|
1461
|
+
const githubUsername = process.env.GITHUB_USERNAME || 'underpostnet';
|
|
1462
|
+
|
|
1463
|
+
if (!githubToken) {
|
|
1464
|
+
logger.warn('backupPodRepositories: GITHUB_TOKEN not available — git push will fail');
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// Resolve the active blue/green traffic colour so we target the correct pod
|
|
1468
|
+
const traffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace });
|
|
1469
|
+
if (!traffic) {
|
|
1470
|
+
logger.warn(`backupPodRepositories: could not resolve current traffic for ${deployId} — skipping`);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Find a running pod that matches the active traffic colour
|
|
1475
|
+
const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
|
|
1476
|
+
const runningPod = pods.find((p) => p.STATUS === 'Running');
|
|
1477
|
+
if (!runningPod) {
|
|
1478
|
+
logger.warn(`backupPodRepositories: no running ${traffic} pod found for ${deployId} in namespace ${namespace}`);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
const podName = runningPod.NAME;
|
|
1482
|
+
|
|
1483
|
+
for (const host of Object.keys(confServer)) {
|
|
1484
|
+
for (const routePath of Object.keys(confServer[host])) {
|
|
1485
|
+
const entry = confServer[host][routePath];
|
|
1486
|
+
if (!entry.repository) continue;
|
|
1487
|
+
|
|
1488
|
+
const siteRoot = Underpost.repo.runtimeSiteRoot(entry.runtime, host);
|
|
1489
|
+
if (!siteRoot) {
|
|
1490
|
+
logger.warn(`backupPodRepositories: no site-root mapping for runtime '${entry.runtime}' (${host})`);
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const repoName = entry.repository.split('/').pop().split('.')[0];
|
|
1495
|
+
|
|
1496
|
+
// Build the backup script — secrets are injected as env vars in the exec,
|
|
1497
|
+
// never written to filesystem. The shell process inherits them ephemerally.
|
|
1498
|
+
const backupScript = [
|
|
1499
|
+
`export GITHUB_TOKEN='${githubToken.replace(/'/g, "'\\''")}'`,
|
|
1500
|
+
`export GITHUB_USERNAME='${githubUsername.replace(/'/g, "'\\''")}'`,
|
|
1501
|
+
`git config --global --add safe.directory '${siteRoot}' 2>/dev/null || true`,
|
|
1502
|
+
`cd '${siteRoot}' && git add -A && git commit -m 'backup $(date -u +%Y-%m-%dT%H:%M:%SZ)' || true`,
|
|
1503
|
+
`cd '${siteRoot}' && underpost push . ${githubUsername}/${repoName}`,
|
|
1504
|
+
].join(' && ');
|
|
1505
|
+
|
|
1506
|
+
try {
|
|
1507
|
+
logger.info(`backupPodRepositories: backing up ${host} (${entry.runtime}) in pod ${podName}`);
|
|
1508
|
+
Underpost.kubectl.exec({ podName, namespace, command: backupScript });
|
|
1509
|
+
logger.info(`backupPodRepositories: git push done for ${host}`);
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
logger.error(`backupPodRepositories: backup failed for ${host}`, err.message);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
},
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Clones the deploy-specific private repository into `./engine-private`
|
|
1519
|
+
* when it does not already exist on disk. Returns `{ ephemeral: true }`
|
|
1520
|
+
* when a fresh clone was performed so the caller can remove it after use,
|
|
1521
|
+
* or `{ ephemeral: false }` when the directory was already present (host,
|
|
1522
|
+
* cron hostPath mount, or prior build step).
|
|
1523
|
+
*
|
|
1524
|
+
* @param {string} [deployId] - Deploy ID (e.g. `dd-core`) used to derive
|
|
1525
|
+
* the repo name `engine-{component}-private`. Falls back to
|
|
1526
|
+
* `process.env.DEFAULT_DEPLOY_ID`.
|
|
1527
|
+
* @returns {{ ephemeral: boolean }}
|
|
1528
|
+
* @memberof UnderpostRepository
|
|
1529
|
+
*/
|
|
1530
|
+
privateEngineRepoFactory(deployId) {
|
|
1531
|
+
if (fs.existsSync('./engine-private')) return { ephemeral: false };
|
|
1532
|
+
|
|
1533
|
+
const effectiveDeployId = deployId || process.env.DEFAULT_DEPLOY_ID;
|
|
1534
|
+
if (!effectiveDeployId) {
|
|
1535
|
+
throw new Error('privateEngineRepoFactory: no deployId provided and DEFAULT_DEPLOY_ID not set');
|
|
1536
|
+
}
|
|
1537
|
+
const username = process.env.GITHUB_USERNAME;
|
|
1538
|
+
if (!username) {
|
|
1539
|
+
throw new Error('privateEngineRepoFactory: GITHUB_USERNAME not set');
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const component = effectiveDeployId.split('-')[1];
|
|
1543
|
+
const repoName = `engine-${component}-private`;
|
|
1544
|
+
logger.info(`engine-private missing — cloning ${username}/${repoName} (ephemeral)`);
|
|
1545
|
+
shellExec(`underpost clone ${username}/${repoName}`);
|
|
1546
|
+
if (!fs.existsSync(`./${repoName}`)) {
|
|
1547
|
+
throw new Error(`privateEngineRepoFactory: clone failed for ${username}/${repoName}`);
|
|
1548
|
+
}
|
|
1549
|
+
shellExec(`mv ./${repoName} ./engine-private`);
|
|
1550
|
+
|
|
1551
|
+
return { ephemeral: true };
|
|
1552
|
+
},
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Removes the ephemeral `engine-private/` clone created by
|
|
1556
|
+
* `privateEngineRepoFactory()`. No-op if the directory does not exist.
|
|
1557
|
+
* @memberof UnderpostRepository
|
|
1558
|
+
*/
|
|
1559
|
+
cleanupPrivateEngineRepo() {
|
|
1560
|
+
if (fs.existsSync('./engine-private')) {
|
|
1561
|
+
fs.removeSync('./engine-private');
|
|
1562
|
+
logger.info('engine-private ephemeral clone removed');
|
|
1563
|
+
}
|
|
1564
|
+
if (fs.existsSync('/home/dd/engine-private')) {
|
|
1565
|
+
fs.removeSync('/home/dd/engine-private');
|
|
1566
|
+
logger.info('engine-private in /home/dd removed');
|
|
1567
|
+
}
|
|
1568
|
+
},
|
|
1426
1569
|
};
|
|
1427
1570
|
}
|
|
1428
1571
|
|
package/src/cli/run.js
CHANGED
|
@@ -1781,7 +1781,7 @@ EOF
|
|
|
1781
1781
|
: [
|
|
1782
1782
|
`npm install -g npm@11.2.0`,
|
|
1783
1783
|
`npm install -g underpost`,
|
|
1784
|
-
`${baseCommand} secret underpost --create-from-
|
|
1784
|
+
`${baseCommand} secret underpost --create-from-env`,
|
|
1785
1785
|
`${baseCommand} start --build --run ${deployId} ${env}`,
|
|
1786
1786
|
];
|
|
1787
1787
|
shellExec(`node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none dd-test --cmd "${cmd}"`);
|
package/src/cli/secrets.js
CHANGED
|
@@ -8,6 +8,10 @@ import { shellExec } from '../server/process.js';
|
|
|
8
8
|
import fs from 'fs-extra';
|
|
9
9
|
import dotenv from 'dotenv';
|
|
10
10
|
import Underpost from '../index.js';
|
|
11
|
+
import { loadConf } from '../server/conf.js';
|
|
12
|
+
import { loggerFactory } from '../server/logger.js';
|
|
13
|
+
|
|
14
|
+
const logger = loggerFactory(import.meta);
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* @class UnderpostSecret
|
|
@@ -23,11 +27,80 @@ class UnderpostSecret {
|
|
|
23
27
|
*/
|
|
24
28
|
underpost: {
|
|
25
29
|
createFromEnvFile(envPath) {
|
|
30
|
+
Underpost.env.clean();
|
|
26
31
|
const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
27
32
|
for (const key of Object.keys(envObj)) {
|
|
28
33
|
Underpost.env.set(key, envObj[key]);
|
|
29
34
|
}
|
|
30
35
|
},
|
|
36
|
+
/** Reads application secrets from process.env (injected via envFrom: secretRef)
|
|
37
|
+
* and writes them to the underpost .env file, filtering out known system and
|
|
38
|
+
* Kubernetes-injected environment variables. Replaces the fragile shell-based
|
|
39
|
+
* `printenv | grep -vE` pattern with a maintainable Node.js blocklist.
|
|
40
|
+
*/
|
|
41
|
+
createFromContainerEnv() {
|
|
42
|
+
Underpost.env.clean();
|
|
43
|
+
const systemKeys = new Set([
|
|
44
|
+
'HOME',
|
|
45
|
+
'HOSTNAME',
|
|
46
|
+
'PATH',
|
|
47
|
+
'TERM',
|
|
48
|
+
'SHLVL',
|
|
49
|
+
'PWD',
|
|
50
|
+
'_',
|
|
51
|
+
'LANG',
|
|
52
|
+
'LANGUAGE',
|
|
53
|
+
'LC_ALL',
|
|
54
|
+
'container',
|
|
55
|
+
'SHELL',
|
|
56
|
+
'USER',
|
|
57
|
+
'LOGNAME',
|
|
58
|
+
'MAIL',
|
|
59
|
+
'OLDPWD',
|
|
60
|
+
'LESSOPEN',
|
|
61
|
+
'LESSCLOSE',
|
|
62
|
+
'LS_COLORS',
|
|
63
|
+
'DISPLAY',
|
|
64
|
+
'COLORTERM',
|
|
65
|
+
'EDITOR',
|
|
66
|
+
'VISUAL',
|
|
67
|
+
'TERM_PROGRAM',
|
|
68
|
+
'TERM_PROGRAM_VERSION',
|
|
69
|
+
'SSH_AUTH_SOCK',
|
|
70
|
+
'SSH_CLIENT',
|
|
71
|
+
'SSH_CONNECTION',
|
|
72
|
+
'SSH_TTY',
|
|
73
|
+
'XDG_SESSION_ID',
|
|
74
|
+
'XDG_RUNTIME_DIR',
|
|
75
|
+
'XDG_DATA_DIRS',
|
|
76
|
+
'XDG_CONFIG_DIRS',
|
|
77
|
+
'DBUS_SESSION_BUS_ADDRESS',
|
|
78
|
+
'GPG_AGENT_INFO',
|
|
79
|
+
'WINDOWID',
|
|
80
|
+
'DESKTOP_SESSION',
|
|
81
|
+
'SESSION_MANAGER',
|
|
82
|
+
'XAUTHORITY',
|
|
83
|
+
'WAYLAND_DISPLAY',
|
|
84
|
+
'which_declare',
|
|
85
|
+
]);
|
|
86
|
+
const systemKeyPrefixes = ['KUBERNETES_', 'npm_', 'NODE_'];
|
|
87
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
88
|
+
if (systemKeys.has(key)) continue;
|
|
89
|
+
if (systemKeyPrefixes.some((prefix) => key.startsWith(prefix))) continue;
|
|
90
|
+
Underpost.env.set(key, value);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Removes all filesystem traces of secrets after deployment startup.
|
|
97
|
+
* Centralizes the defense-in-depth cleanup performed
|
|
98
|
+
* @memberof UnderpostSecret
|
|
99
|
+
*/
|
|
100
|
+
globalSecretClean() {
|
|
101
|
+
loadConf('clean');
|
|
102
|
+
Underpost.repo.cleanupPrivateEngineRepo();
|
|
103
|
+
Underpost.env.clean();
|
|
31
104
|
},
|
|
32
105
|
};
|
|
33
106
|
}
|
|
@@ -75,14 +75,22 @@ const DropDown = {
|
|
|
75
75
|
console.log('DropDown onClick', this.value);
|
|
76
76
|
if (options && options.resetOnClick) options.resetOnClick();
|
|
77
77
|
if (options && options.type === 'checkbox') {
|
|
78
|
+
DropDown.Tokens[id].oncheckvalues = {};
|
|
79
|
+
DropDown.Tokens[id].value = [];
|
|
80
|
+
s(`.${id}`).value = [];
|
|
81
|
+
htmls(`.dropdown-current-${id}`, '');
|
|
78
82
|
if (options.serviceProvider) {
|
|
79
|
-
DropDown.Tokens[id].oncheckvalues = {};
|
|
80
|
-
DropDown.Tokens[id].value = [];
|
|
81
|
-
htmls(`.dropdown-current-${id}`, '');
|
|
82
83
|
htmls(`.${id}-render-container`, '');
|
|
83
84
|
} else {
|
|
84
|
-
for (const
|
|
85
|
-
|
|
85
|
+
for (const optionData of options.data) {
|
|
86
|
+
if (optionData.value !== 'reset' && optionData.value !== 'close' && optionData.checked) {
|
|
87
|
+
optionData.checked = false;
|
|
88
|
+
const vd = optionData.value.trim().replaceAll(' ', '-');
|
|
89
|
+
if (ToggleSwitch.Tokens[`checkbox-role-${vd}`]) {
|
|
90
|
+
const checkbox = s(`.checkbox-role-${vd}-checkbox`);
|
|
91
|
+
if (checkbox && checkbox.checked) ToggleSwitch.Tokens[`checkbox-role-${vd}`].click();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
96
|
} else this.Tokens[id].value = undefined;
|
package/src/index.js
CHANGED
|
@@ -30,6 +30,10 @@ RUN dnf clean all
|
|
|
30
30
|
RUN node --version
|
|
31
31
|
RUN npm --version
|
|
32
32
|
|
|
33
|
+
# Create non-root user for secure container execution (cron jobs, init containers)
|
|
34
|
+
# Deployment containers override to root via securityContext when npm install -g is needed
|
|
35
|
+
RUN useradd -m -u 1000 -s /bin/bash dd
|
|
36
|
+
|
|
33
37
|
# Set working directory
|
|
34
38
|
WORKDIR /home/dd
|
|
35
39
|
|
|
@@ -47,6 +47,10 @@ RUN dnf clean all
|
|
|
47
47
|
RUN node --version
|
|
48
48
|
RUN npm --version
|
|
49
49
|
|
|
50
|
+
# Create non-root user for secure container execution (cron jobs, init containers)
|
|
51
|
+
# Deployment containers override to root via securityContext when npm install -g is needed
|
|
52
|
+
RUN useradd -m -u 1000 -s /bin/bash dd
|
|
53
|
+
|
|
50
54
|
# Set working directory
|
|
51
55
|
WORKDIR /home/dd
|
|
52
56
|
|
|
@@ -239,13 +239,35 @@ Listen ${port}
|
|
|
239
239
|
DocumentRoot "${documentRoot}"
|
|
240
240
|
ServerName ${host}
|
|
241
241
|
UseCanonicalName Off
|
|
242
|
+
ServerSignature Off
|
|
242
243
|
|
|
243
244
|
<Directory "${documentRoot}">
|
|
244
|
-
Options
|
|
245
|
+
Options FollowSymLinks MultiViews
|
|
245
246
|
AllowOverride All
|
|
246
247
|
Require all granted
|
|
247
248
|
</Directory>
|
|
248
249
|
|
|
250
|
+
# Block access to .git directories and files
|
|
251
|
+
RedirectMatch 404 /\\.git
|
|
252
|
+
|
|
253
|
+
# Block access to hidden dotfiles (.env, .htpasswd, etc.)
|
|
254
|
+
<FilesMatch "^\\.(env|htpasswd|htaccess\\.bak|DS_Store)">
|
|
255
|
+
Require all denied
|
|
256
|
+
</FilesMatch>
|
|
257
|
+
|
|
258
|
+
# Block access to sensitive engine files
|
|
259
|
+
<FilesMatch "(wp-config\\.php\\.bak|wp-config-sample\\.php|\\.sql|\\.sql\\.gz)$">
|
|
260
|
+
Require all denied
|
|
261
|
+
</FilesMatch>
|
|
262
|
+
|
|
263
|
+
# Security headers
|
|
264
|
+
<IfModule mod_headers.c>
|
|
265
|
+
Header always set X-Content-Type-Options "nosniff"
|
|
266
|
+
Header always set X-Frame-Options "SAMEORIGIN"
|
|
267
|
+
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
|
268
|
+
Header always unset X-Powered-By
|
|
269
|
+
</IfModule>
|
|
270
|
+
|
|
249
271
|
${
|
|
250
272
|
redirect
|
|
251
273
|
? `
|
|
@@ -53,6 +53,10 @@ RUN dnf clean all
|
|
|
53
53
|
RUN node --version
|
|
54
54
|
RUN npm --version
|
|
55
55
|
|
|
56
|
+
# Create non-root user for secure container execution (cron jobs, init containers)
|
|
57
|
+
# Deployment containers override to root via securityContext when npm install -g is needed
|
|
58
|
+
RUN useradd -m -u 1000 -s /bin/bash dd
|
|
59
|
+
|
|
56
60
|
# Set working directory
|
|
57
61
|
WORKDIR /home/dd
|
|
58
62
|
|