underpost 3.2.80 → 3.2.90

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +182 -1
  2. package/CLI-HELP.md +37 -16
  3. package/README.md +2 -2
  4. package/bin/deploy.js +18 -16
  5. package/docker-compose.yml +1 -1
  6. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  7. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  8. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  9. package/manifests/deployment/playwright/deployment.yaml +1 -1
  10. package/manifests/mongodb/kustomization.yaml +4 -1
  11. package/manifests/mongodb/statefulset.yaml +4 -0
  12. package/manifests/mongodb/storage-class.yaml +9 -2
  13. package/package.json +17 -17
  14. package/scripts/nat-iptables.sh +10 -4
  15. package/scripts/test-monitor.sh +4 -3
  16. package/src/cli/cluster.js +740 -55
  17. package/src/cli/db.js +2 -2
  18. package/src/cli/deploy.js +1679 -174
  19. package/src/cli/docker-compose.js +19 -178
  20. package/src/cli/image.js +15 -6
  21. package/src/cli/index.js +124 -35
  22. package/src/cli/ipfs.js +82 -11
  23. package/src/cli/monitor.js +1 -1
  24. package/src/cli/repository.js +1 -1
  25. package/src/cli/run.js +2161 -420
  26. package/src/cli/secrets.js +969 -0
  27. package/src/cli/ssh.js +8 -28
  28. package/src/client-builder/client-build.js +94 -11
  29. package/src/client-builder/ssr.js +27 -73
  30. package/src/db/mongo/MongoBootstrap.js +295 -54
  31. package/src/db/mongo/MongooseDB.js +47 -32
  32. package/src/index.js +1 -1
  33. package/src/server/conf.js +1208 -70
  34. package/src/server/cri.js +70 -0
  35. package/src/server/underpost-gateway.js +1073 -0
  36. package/src/server/underpost-ingress.js +364 -0
  37. package/test/cluster-instances.test.js +435 -0
  38. package/test/deploy-node-placement.test.js +45 -0
  39. package/test/instance-traffic-plan.test.js +710 -0
  40. package/test/sops-secret-store.test.js +612 -0
  41. package/test/underpost-gateway.test.js +469 -0
  42. package/test/underpost-ingress.test.js +253 -0
package/src/cli/ipfs.js CHANGED
@@ -6,11 +6,14 @@
6
6
 
7
7
  import { loggerFactory } from '../server/logger.js';
8
8
  import { shellExec } from '../server/process.js';
9
+ import { resolveReplicaCount } from '../server/conf.js';
9
10
  import fs from 'fs-extra';
10
11
  import Underpost from '../index.js';
11
12
 
12
13
  const logger = loggerFactory(import.meta);
13
14
 
15
+ const IPFS_DEFAULT_REPLICA_COUNT = 3;
16
+
14
17
  /**
15
18
  * @class UnderpostIPFS
16
19
  * @description Manages deployment of an ipfs-cluster StatefulSet on Kubernetes.
@@ -25,8 +28,12 @@ class UnderpostIPFS {
25
28
  * @description Resolves the IPFS cluster credentials from engine-private/ if they
26
29
  * already exist, otherwise generates new ones (hex cluster secret + peer identity
27
30
  * via ipfs-cluster-service init) and persists them with mode 0o600.
31
+ * Idempotent and self-healing: an existing pair is reused untouched, and a missing or
32
+ * unreadable one is regenerated. Both files are always rewritten together — the peer id and
33
+ * the private key are two halves of one identity, so reusing one with a freshly minted other
34
+ * would advertise a peer id that does not match the key.
28
35
  * @param {string} privateDir - Absolute path to the engine-private directory.
29
- * @returns {{ CLUSTER_SECRET: string, IDENTITY_JSON: { id: string, private_key: string } }}
36
+ * @returns {{ CLUSTER_SECRET: string, IDENTITY_JSON: { id: string, private_key: string }, generated: boolean }}
30
37
  * @memberof UnderpostIPFS
31
38
  */
32
39
  resolveCredentials(privateDir) {
@@ -34,11 +41,17 @@ class UnderpostIPFS {
34
41
  const identityPath = `${privateDir}/ipfs-cluster-identity.json`;
35
42
 
36
43
  if (fs.existsSync(secretPath) && fs.existsSync(identityPath)) {
37
- logger.info('Reusing existing IPFS cluster credentials from engine-private/');
38
- return {
39
- CLUSTER_SECRET: fs.readFileSync(secretPath, 'utf8').trim(),
40
- IDENTITY_JSON: JSON.parse(fs.readFileSync(identityPath, 'utf8')),
41
- };
44
+ try {
45
+ const CLUSTER_SECRET = fs.readFileSync(secretPath, 'utf8').trim();
46
+ const IDENTITY_JSON = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
47
+ if (CLUSTER_SECRET && IDENTITY_JSON?.id && IDENTITY_JSON?.private_key) {
48
+ logger.info('Reusing existing IPFS cluster credentials from engine-private/');
49
+ return { CLUSTER_SECRET, IDENTITY_JSON, generated: false };
50
+ }
51
+ logger.warn('Existing IPFS cluster credentials are incomplete; regenerating');
52
+ } catch (error) {
53
+ logger.warn(`Existing IPFS cluster credentials are unreadable (${error.message}); regenerating`);
54
+ }
42
55
  }
43
56
 
44
57
  logger.info('Generating new IPFS cluster credentials and persisting to engine-private/');
@@ -61,7 +74,52 @@ class UnderpostIPFS {
61
74
 
62
75
  logger.info(`IPFS cluster credentials saved (peer ID: ${IDENTITY_JSON.id})`);
63
76
 
64
- return { CLUSTER_SECRET, IDENTITY_JSON };
77
+ return { CLUSTER_SECRET, IDENTITY_JSON, generated: true };
78
+ },
79
+
80
+ /**
81
+ * @method storeCredentials
82
+ * @description Encrypts the current IPFS credentials into the SOPS store, replacing whatever
83
+ * is there. Called after a regeneration so the encrypted Secret cannot drift from the peer id
84
+ * the `env-config` ConfigMap advertises — the id lives only in the local identity file, so a
85
+ * stale manifest would pair someone else's private key with the new id and the cluster would
86
+ * never form.
87
+ * @param {{ CLUSTER_SECRET: string, IDENTITY_JSON: { private_key: string } }} credentials
88
+ * @param {object} options
89
+ * @param {string} options.namespace - Kubernetes namespace.
90
+ * @memberof UnderpostIPFS
91
+ */
92
+ storeCredentials({ CLUSTER_SECRET, IDENTITY_JSON }, options) {
93
+ const stageDir = '/dev/shm/underpost-secrets';
94
+ const stagePath = `${stageDir}/ipfs-cluster-secret.yaml`;
95
+ fs.ensureDirSync(stageDir);
96
+ fs.chmodSync(stageDir, 0o700);
97
+ try {
98
+ fs.outputFileSync(
99
+ stagePath,
100
+ [
101
+ 'apiVersion: v1',
102
+ 'kind: Secret',
103
+ 'metadata:',
104
+ ' name: ipfs-cluster-secret',
105
+ ` namespace: ${options.namespace}`,
106
+ ' labels:',
107
+ ' app.kubernetes.io/managed-by: underpost',
108
+ 'type: Opaque',
109
+ 'stringData:',
110
+ ` cluster-secret: '${CLUSTER_SECRET.replace(/'/g, "''")}'`,
111
+ ` bootstrap-peer-priv-key: '${IDENTITY_JSON.private_key.replace(/'/g, "''")}'`,
112
+ '',
113
+ ].join('\n'),
114
+ 'utf8',
115
+ );
116
+ fs.chmodSync(stagePath, 0o600);
117
+ // encrypt() stages, validates, moves into place, and shreds the plaintext source.
118
+ Underpost.secret.sops.encrypt(stagePath, options.namespace, { force: true });
119
+ logger.info('Re-encrypted regenerated IPFS credentials into the SOPS store');
120
+ } finally {
121
+ fs.removeSync(stageDir);
122
+ }
65
123
  },
66
124
 
67
125
  /**
@@ -100,12 +158,18 @@ class UnderpostIPFS {
100
158
  applySecrets({ CLUSTER_SECRET, IDENTITY_JSON }, options) {
101
159
  logger.info('Applying IPFS cluster Kubernetes Secret and env ConfigMap');
102
160
 
103
- shellExec(
104
- `kubectl create secret generic ipfs-cluster-secret \
161
+ // Encrypted store first, origin generate/read logic only when no manifest is stored.
162
+ // `--from-literal` places the cluster secret and the peer private key in the command
163
+ // string, so the seed path is kept out of the command log; the encrypted path never
164
+ // exposes them at all.
165
+ if (!Underpost.secret.sops.applyIfPresent('ipfs-cluster-secret', options.namespace))
166
+ shellExec(
167
+ `kubectl create secret generic ipfs-cluster-secret \
105
168
  --from-literal=cluster-secret=${CLUSTER_SECRET} \
106
169
  --from-literal=bootstrap-peer-priv-key=${IDENTITY_JSON.private_key} \
107
170
  --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
108
- );
171
+ { disableLog: true },
172
+ );
109
173
 
110
174
  shellExec(
111
175
  `kubectl create configmap env-config \
@@ -165,7 +229,14 @@ sudo sysctl -w net.core.wmem_max=7500000`,
165
229
 
166
230
  const credentials = Underpost.ipfs.resolveCredentials(`${underpostRoot}/engine-private`);
167
231
 
168
- const ipfsReplicas = options.replicas ? parseInt(options.replicas) : 3;
232
+ // `env-config` advertises `bootstrap-peer-id` from the local identity file — the peer id is
233
+ // not carried in the Secret. So a regeneration invalidates any stored manifest: it would
234
+ // pair the previous private key with the new id and the cluster would never form. Re-encrypt
235
+ // the fresh pair so the store and the ConfigMap stay one identity.
236
+ if (credentials.generated && Underpost.secret.sops.has('ipfs-cluster-secret', options.namespace))
237
+ Underpost.ipfs.storeCredentials(credentials, options);
238
+
239
+ const ipfsReplicas = resolveReplicaCount(options.replicas, IPFS_DEFAULT_REPLICA_COUNT);
169
240
 
170
241
  Underpost.ipfs.teardown(options, ipfsReplicas);
171
242
  Underpost.ipfs.applySecrets(credentials, options);
@@ -123,7 +123,7 @@ class UnderpostMonitor {
123
123
 
124
124
  let errorPayloads = [];
125
125
  if (options.sync === true) {
126
- const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
126
+ const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace, env });
127
127
  if (currentTraffic) Underpost.env.set(`${deployId}-${env}-traffic`, currentTraffic);
128
128
  }
129
129
  let traffic = Underpost.env.get(`${deployId}-${env}-traffic`) ?? 'blue';
@@ -1706,7 +1706,7 @@ Prevent build private config repo.`,
1706
1706
  }
1707
1707
 
1708
1708
  // Resolve the active blue/green traffic colour so we target the correct pod
1709
- const traffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace });
1709
+ const traffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace, env });
1710
1710
  if (!traffic) {
1711
1711
  logger.warn(`backupPodRepositories: could not resolve current traffic for ${deployId} — skipping`);
1712
1712
  return;