underpost 3.2.70 → 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 (60) hide show
  1. package/.github/workflows/publish.ci.yml +3 -3
  2. package/.github/workflows/release.cd.yml +1 -1
  3. package/CHANGELOG.md +1358 -1038
  4. package/CLI-HELP.md +39 -16
  5. package/README.md +3 -3
  6. package/bin/build.js +10 -4
  7. package/bin/deploy.js +18 -16
  8. package/docker-compose.yml +1 -1
  9. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  10. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  11. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  12. package/manifests/deployment/playwright/deployment.yaml +1 -1
  13. package/manifests/mongodb/kustomization.yaml +4 -1
  14. package/manifests/mongodb/statefulset.yaml +4 -0
  15. package/manifests/mongodb/storage-class.yaml +9 -2
  16. package/package.json +20 -20
  17. package/scripts/nat-iptables.sh +10 -4
  18. package/scripts/test-monitor.sh +4 -3
  19. package/src/api/core/core.controller.js +4 -65
  20. package/src/api/core/core.router.js +8 -14
  21. package/src/api/default/default.controller.js +2 -70
  22. package/src/api/default/default.router.js +7 -17
  23. package/src/api/document/document.controller.js +5 -77
  24. package/src/api/document/document.router.js +9 -13
  25. package/src/api/file/file.controller.js +9 -53
  26. package/src/api/file/file.router.js +14 -6
  27. package/src/api/test/test.controller.js +8 -53
  28. package/src/api/test/test.router.js +1 -4
  29. package/src/cli/cluster.js +771 -66
  30. package/src/cli/db.js +6 -4
  31. package/src/cli/deploy.js +1715 -168
  32. package/src/cli/docker-compose.js +19 -24
  33. package/src/cli/fs.js +0 -1
  34. package/src/cli/image.js +40 -13
  35. package/src/cli/index.js +129 -35
  36. package/src/cli/ipfs.js +82 -11
  37. package/src/cli/monitor.js +1 -1
  38. package/src/cli/release.js +4 -0
  39. package/src/cli/repository.js +14 -3
  40. package/src/cli/run.js +2253 -439
  41. package/src/cli/secrets.js +969 -0
  42. package/src/cli/ssh.js +38 -39
  43. package/src/client/components/core/Modal.js +38 -4
  44. package/src/client-builder/client-build.js +94 -11
  45. package/src/client-builder/ssr.js +27 -73
  46. package/src/db/mongo/MongoBootstrap.js +295 -54
  47. package/src/db/mongo/MongooseDB.js +47 -32
  48. package/src/index.js +1 -1
  49. package/src/server/conf.js +1307 -6
  50. package/src/server/cri.js +70 -0
  51. package/src/server/downloader.js +3 -3
  52. package/src/server/middlewares.js +152 -0
  53. package/src/server/underpost-gateway.js +1073 -0
  54. package/src/server/underpost-ingress.js +364 -0
  55. package/test/cluster-instances.test.js +435 -0
  56. package/test/deploy-node-placement.test.js +45 -0
  57. package/test/instance-traffic-plan.test.js +710 -0
  58. package/test/sops-secret-store.test.js +612 -0
  59. package/test/underpost-gateway.test.js +469 -0
  60. package/test/underpost-ingress.test.js +253 -0
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Underpost ingress: the shared entry point that lets the Contour and Gateway
3
+ * API data planes run side by side.
4
+ *
5
+ * Only one process can hold a node's 80/443, and a `hostPort` claim is stronger
6
+ * than a listener: the CNI hostport plugin DNATs every packet on those ports to
7
+ * the claiming pod before anything else sees them. So two ingress stacks on one
8
+ * node is not a configuration question — whichever claims first silently takes
9
+ * all traffic, and the other logs nothing at all.
10
+ *
11
+ * This module owns the port instead, and hands each connection to the data plane
12
+ * that actually describes its hostname. Both stacks are then reachable through
13
+ * the same address, and neither has to be uninstalled to try the other.
14
+ *
15
+ * Cleartext and TLS are split deliberately:
16
+ *
17
+ * - `:80` is proxied at L7, because a plaintext request carries a readable `Host`
18
+ * header and the backends answer it with their own redirect or content.
19
+ * - `:443` is forwarded at L4 by SNI, because terminating TLS here would mean
20
+ * holding every host's certificate and re-negotiating ALPN. Passing the bytes
21
+ * through keeps certificates, HTTP/2 and mTLS exactly where they already work.
22
+ *
23
+ * @module src/server/underpost-ingress.js
24
+ * @namespace UnderpostIngress
25
+ */
26
+
27
+ /**
28
+ * @constant UNDERPOST_INGRESS
29
+ * @description Identity of the underpost ingress workload. One deployment fronts every
30
+ * data plane on the node, so these names are cluster-wide constants.
31
+ * @memberof UnderpostIngress
32
+ */
33
+ const UNDERPOST_INGRESS = {
34
+ name: 'underpost-ingress',
35
+ configMapName: 'underpost-ingress-nginx',
36
+ image: 'nginx:alpine',
37
+ httpPort: 80,
38
+ httpsPort: 443,
39
+ healthPort: 8090,
40
+ healthPath: '/healthz',
41
+ // kube-dns's conventional ClusterIP; overridden from the live Service.
42
+ resolver: '10.96.0.10',
43
+ backends: {
44
+ contour: { http: 'envoy.projectcontour.svc.cluster.local:80', tls: 'envoy.projectcontour.svc.cluster.local:443' },
45
+ },
46
+ };
47
+
48
+ /**
49
+ * @method gatewayBackendFactory
50
+ * @description Upstreams for the Gateway API data plane.
51
+ *
52
+ * Envoy Gateway provisions and names its own Service (`envoy-<class>-<hash>`),
53
+ * so unlike Contour's fixed name this one is discovered from the cluster and
54
+ * passed in.
55
+ * @param {string} service - Provisioned Service name.
56
+ * @param {string} [namespace] - Namespace holding it.
57
+ * @returns {{http: string, tls: string}} Qualified upstreams.
58
+ * @memberof UnderpostIngress
59
+ */
60
+ const gatewayBackendFactory = (service, namespace = 'envoy-gateway-system') => ({
61
+ http: `${service}.${namespace}.svc.cluster.local:80`,
62
+ tls: `${service}.${namespace}.svc.cluster.local:443`,
63
+ });
64
+
65
+ /**
66
+ * @method underpostIngressHostMapFactory
67
+ * @description Decides which data plane each hostname is handed to.
68
+ *
69
+ * A hostname belongs to whichever stack has a route object describing it, which
70
+ * is the only fact that survives a switch between them: manifests for both kinds
71
+ * are always generated, so the objects that actually exist in the cluster are
72
+ * what says who serves what.
73
+ *
74
+ * A hostname described by both is a leftover from switching stacks, not a valid
75
+ * state — both would answer, and which one won would depend on ordering here. It
76
+ * resolves to `preferred` and is reported separately so the duplicate can be
77
+ * removed rather than silently tolerated.
78
+ * @param {Array<string>} [contourHosts] - Hostnames with an HTTPProxy.
79
+ * @param {Array<string>} [gatewayHosts] - Hostnames with an HTTPRoute.
80
+ * @param {string} [preferred] - Backend that wins a hostname described by both.
81
+ * @returns {{entries: Array<{host: string, backend: string}>, conflicts: Array<string>}} Routing table and duplicates.
82
+ * @memberof UnderpostIngress
83
+ */
84
+ const underpostIngressHostMapFactory = ({ contourHosts = [], gatewayHosts = [], preferred = 'gateway' } = {}) => {
85
+ const contour = new Set(contourHosts.filter(Boolean));
86
+ const gateway = new Set(gatewayHosts.filter(Boolean));
87
+ const conflicts = [...contour].filter((host) => gateway.has(host)).sort();
88
+ const entries = [...new Set([...contour, ...gateway])].sort().map((host) => ({
89
+ host,
90
+ backend: contour.has(host) && gateway.has(host) ? preferred : contour.has(host) ? 'contour' : 'gateway',
91
+ }));
92
+ return { entries, conflicts };
93
+ };
94
+
95
+ /**
96
+ * @method underpostIngressConfFactory
97
+ * @description Renders the underpost ingress Nginx configuration.
98
+ *
99
+ * `default` in both maps is what an unknown hostname reaches. It is the stack
100
+ * that owns most of the routing rather than a rejection, so a host whose route
101
+ * object has not been applied yet still reaches a data plane that can answer it
102
+ * — including with its own 404.
103
+ * @param {Array<object>} [entries] - Host table from {@link UnderpostIngress.underpostIngressHostMapFactory}.
104
+ * @param {object} backends - `{contour: {http, tls}, gateway: {http, tls}}`; a missing stack is omitted.
105
+ * @param {string} [defaultBackend] - Backend for an unmatched hostname.
106
+ * @param {string} [resolver] - Cluster DNS ClusterIP.
107
+ * @returns {string} nginx.conf contents.
108
+ * @memberof UnderpostIngress
109
+ */
110
+ const underpostIngressConfFactory = ({
111
+ entries = [],
112
+ backends = {},
113
+ defaultBackend = 'gateway',
114
+ resolver = UNDERPOST_INGRESS.resolver,
115
+ } = {}) => {
116
+ const available = Object.keys(backends).filter((name) => backends[name]?.http && backends[name]?.tls);
117
+ // With one stack installed the map still renders, so the same workload serves
118
+ // the single-stack case without a second code path.
119
+ const fallback = available.includes(defaultBackend) ? defaultBackend : available[0];
120
+ if (!fallback) throw new Error('[underpost-ingress] No data plane backend to route to');
121
+ const routable = entries.filter((entry) => entry?.host && available.includes(entry.backend));
122
+ const mapEntries = (kind) =>
123
+ routable.map((entry) => ` ${entry.host} ${backends[entry.backend][kind]};`).join('\n') || '';
124
+
125
+ return `worker_processes auto;
126
+ error_log /dev/stderr warn;
127
+ pid /tmp/nginx.pid;
128
+
129
+ events {
130
+ worker_connections 4096;
131
+ }
132
+
133
+ http {
134
+ server_tokens off;
135
+ log_format underpost_ingress '$remote_addr "$request" $status "$host" -> $underpost_ingress_http_upstream';
136
+ access_log /dev/stdout underpost_ingress;
137
+
138
+ map $http_upgrade $connection_upgrade {
139
+ default upgrade;
140
+ '' close;
141
+ }
142
+
143
+ # Cluster DNS as a literal address — nginx cannot resolve its own resolver.
144
+ # Every upstream is passed through a variable, so without this nginx resolves
145
+ # each Service name once at start-up and keeps the address for the life of the
146
+ # process; a re-provisioned data plane gets a new ClusterIP.
147
+ resolver ${resolver} valid=10s ipv6=off;
148
+
149
+ map $host $underpost_ingress_http_upstream {
150
+ default ${backends[fallback].http};
151
+ ${mapEntries('http')}
152
+ }
153
+
154
+ server {
155
+ listen ${UNDERPOST_INGRESS.healthPort} default_server;
156
+ server_name _;
157
+ location = ${UNDERPOST_INGRESS.healthPath} {
158
+ access_log off;
159
+ add_header Content-Type text/plain;
160
+ return 200 'ok';
161
+ }
162
+ location / {
163
+ return 404;
164
+ }
165
+ }
166
+
167
+ server {
168
+ listen ${UNDERPOST_INGRESS.httpPort} default_server;
169
+ server_name _;
170
+
171
+ location / {
172
+ proxy_http_version 1.1;
173
+ proxy_set_header Host $host;
174
+ proxy_set_header X-Real-IP $remote_addr;
175
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
176
+ proxy_set_header X-Forwarded-Proto $scheme;
177
+ # Websocket upgrades must survive this hop or a client that negotiated one
178
+ # at the edge is left holding a half-open connection.
179
+ proxy_set_header Upgrade $http_upgrade;
180
+ proxy_set_header Connection $connection_upgrade;
181
+ proxy_pass http://$underpost_ingress_http_upstream;
182
+ }
183
+ }
184
+ }
185
+
186
+ stream {
187
+ log_format underpost_ingress '$remote_addr -> "$ssl_preread_server_name" $upstream_addr $status';
188
+ access_log /dev/stdout underpost_ingress;
189
+ resolver ${resolver} valid=10s ipv6=off;
190
+
191
+ # SNI is read without terminating the connection, so each data plane keeps
192
+ # serving its own certificates and negotiating its own ALPN. Terminating here
193
+ # would mean holding every host's key and re-offering h2 on the way out.
194
+ map $ssl_preread_server_name $underpost_ingress_tls_upstream {
195
+ default ${backends[fallback].tls};
196
+ ${mapEntries('tls')}
197
+ }
198
+
199
+ server {
200
+ listen ${UNDERPOST_INGRESS.httpsPort};
201
+ ssl_preread on;
202
+ proxy_pass $underpost_ingress_tls_upstream;
203
+ }
204
+ ${
205
+ backends.gateway
206
+ ? `
207
+ # QUIC keeps working, but it cannot be routed by hostname: \`ssl_preread\` is
208
+ # TCP-only, and a QUIC Initial carries its SNI inside an encrypted frame. Only
209
+ # the Gateway API data plane serves HTTP/3 here — Contour advertises no
210
+ # Alt-Svc — so every datagram goes there. A client that tries QUIC against a
211
+ # Contour host gets no answer and falls back to TCP, exactly as it would have
212
+ # if nothing had advertised HTTP/3 at all.
213
+ #
214
+ # The constant is reached through a map because a literal \`proxy_pass\` in
215
+ # \`stream\` is resolved when the config is parsed, not through \`resolver\`:
216
+ # nginx refuses to start while the Service has no DNS record, and once it does
217
+ # start it keeps that address for the life of the process.
218
+ map $remote_addr $underpost_ingress_quic_upstream {
219
+ default ${backends.gateway.tls};
220
+ }
221
+
222
+ server {
223
+ listen ${UNDERPOST_INGRESS.httpsPort} udp;
224
+ proxy_pass $underpost_ingress_quic_upstream;
225
+ proxy_timeout 30s;
226
+ }
227
+ `
228
+ : ''
229
+ }}
230
+ `;
231
+ };
232
+
233
+ /**
234
+ * @method underpostIngressManifestsFactory
235
+ * @description Renders the underpost ingress workload.
236
+ *
237
+ * Host-networked, because it exists to be the thing that holds the node's 80/443
238
+ * — the ports both data planes have just been moved off. `hostPort` is not used:
239
+ * the claim it installs is what made the two stacks exclusive in the first place.
240
+ * @param {string} [namespace] - Namespace to deploy into.
241
+ * @param {string} conf - Rendered nginx.conf.
242
+ * @param {string} [nodeName] - Node to pin the underpost ingress to; empty schedules freely.
243
+ * @returns {string} Multi-document YAML.
244
+ * @memberof UnderpostIngress
245
+ */
246
+ const underpostIngressManifestsFactory = ({ namespace = 'default', conf, nodeName = '' } = {}) => {
247
+ return `
248
+ ---
249
+ apiVersion: v1
250
+ kind: ConfigMap
251
+ metadata:
252
+ name: ${UNDERPOST_INGRESS.configMapName}
253
+ namespace: ${namespace}
254
+ data:
255
+ nginx.conf: |
256
+ ${`${conf}`
257
+ .replace(/\n$/, '')
258
+ .split('\n')
259
+ .map((line) => (line.length > 0 ? ` ${line}` : ''))
260
+ .join('\n')}
261
+ ---
262
+ apiVersion: apps/v1
263
+ kind: Deployment
264
+ metadata:
265
+ name: ${UNDERPOST_INGRESS.name}
266
+ namespace: ${namespace}
267
+ labels:
268
+ app: ${UNDERPOST_INGRESS.name}
269
+ spec:
270
+ replicas: 1
271
+ # Recreate is retained for real pod-template upgrades because the node's ports
272
+ # cannot be held twice. Host-table changes do not alter this template: the
273
+ # installer validates and hot-reloads /tmp/nginx.conf in the existing pod.
274
+ strategy:
275
+ type: Recreate
276
+ selector:
277
+ matchLabels:
278
+ app: ${UNDERPOST_INGRESS.name}
279
+ template:
280
+ metadata:
281
+ labels:
282
+ app: ${UNDERPOST_INGRESS.name}
283
+ spec:
284
+ hostNetwork: true
285
+ dnsPolicy: ClusterFirstWithHostNet${
286
+ nodeName
287
+ ? `
288
+ nodeSelector:
289
+ kubernetes.io/hostname: ${nodeName}`
290
+ : ''
291
+ }
292
+ containers:
293
+ - name: nginx
294
+ image: ${UNDERPOST_INGRESS.image}
295
+ # The latest tag otherwise implies Always. Production edge nodes are often
296
+ # deliberately unable to reach Docker Hub; once the audited image is
297
+ # present, a route-table refresh or pod restart must stay offline-safe.
298
+ imagePullPolicy: IfNotPresent
299
+ command:
300
+ - /bin/sh
301
+ - -c
302
+ - cp /etc/underpost-ingress/nginx.conf /tmp/nginx.conf && exec nginx -c /tmp/nginx.conf -g 'daemon off;'
303
+ # No \`ports:\` block, deliberately. Under \`hostNetwork: true\` Kubernetes
304
+ # sets \`hostPort\` to each \`containerPort\`, and the scheduler then
305
+ # refuses to place the pod unless those host ports are already free —
306
+ # which they are not, because the data planes only release them as this
307
+ # workload arrives. Declaring them turns the ports this exists to take
308
+ # into a precondition for being scheduled at all.
309
+ securityContext:
310
+ runAsNonRoot: false
311
+ runAsUser: 0
312
+ allowPrivilegeEscalation: false
313
+ capabilities:
314
+ drop:
315
+ - ALL
316
+ # The master starts as root and needs all four: NET_BIND_SERVICE to
317
+ # bind 80/443, CHOWN to take ownership of its temp paths, and
318
+ # SETUID/SETGID to drop each worker to the unprivileged \`nginx\`
319
+ # user. Dropping any one of them is a start-up failure rather than a
320
+ # degraded mode — and only the first is visible to \`nginx -t\`, since
321
+ # the other two are reached when workers spawn.
322
+ add:
323
+ - NET_BIND_SERVICE
324
+ - CHOWN
325
+ - SETUID
326
+ - SETGID
327
+ readinessProbe:
328
+ httpGet:
329
+ path: ${UNDERPOST_INGRESS.healthPath}
330
+ port: ${UNDERPOST_INGRESS.healthPort}
331
+ initialDelaySeconds: 2
332
+ periodSeconds: 5
333
+ livenessProbe:
334
+ httpGet:
335
+ path: ${UNDERPOST_INGRESS.healthPath}
336
+ port: ${UNDERPOST_INGRESS.healthPort}
337
+ initialDelaySeconds: 10
338
+ periodSeconds: 20
339
+ volumeMounts:
340
+ - name: nginx-conf
341
+ mountPath: /etc/underpost-ingress
342
+ readOnly: true
343
+ - name: cache
344
+ mountPath: /var/cache/nginx
345
+ - name: run
346
+ mountPath: /tmp
347
+ volumes:
348
+ - name: nginx-conf
349
+ configMap:
350
+ name: ${UNDERPOST_INGRESS.configMapName}
351
+ - name: cache
352
+ emptyDir: {}
353
+ - name: run
354
+ emptyDir: {}
355
+ `;
356
+ };
357
+
358
+ export {
359
+ UNDERPOST_INGRESS,
360
+ gatewayBackendFactory,
361
+ underpostIngressConfFactory,
362
+ underpostIngressHostMapFactory,
363
+ underpostIngressManifestsFactory,
364
+ };