alasio 6.0.0 → 8.0.0
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/dist/cli/src/branches.js +106 -31
- package/dist/cli/src/branches.js.map +1 -1
- package/dist/cli/src/cluster/docker-engine.js +6 -4
- package/dist/cli/src/cluster/docker-engine.js.map +1 -1
- package/dist/cli/src/cluster/docker.js +49 -10
- package/dist/cli/src/cluster/docker.js.map +1 -1
- package/dist/cli/src/cluster/host.js +45 -19
- package/dist/cli/src/cluster/host.js.map +1 -1
- package/dist/cli/src/cluster/k3s.js +104 -5
- package/dist/cli/src/cluster/k3s.js.map +1 -1
- package/dist/cli/src/cluster/node.js +102 -51
- package/dist/cli/src/cluster/node.js.map +1 -1
- package/dist/cli/src/cluster/tar.js +43 -1
- package/dist/cli/src/cluster/tar.js.map +1 -1
- package/dist/cli/src/commands/branch.js +7 -3
- package/dist/cli/src/commands/branch.js.map +1 -1
- package/dist/cli/src/commands/init.js +54 -4
- package/dist/cli/src/commands/init.js.map +1 -1
- package/dist/cli/src/commands/lake.js +40 -6
- package/dist/cli/src/commands/lake.js.map +1 -1
- package/dist/cli/src/commands/restore-filer-store.js +56 -13
- package/dist/cli/src/commands/restore-filer-store.js.map +1 -1
- package/dist/cli/src/commands/status.js +182 -10
- package/dist/cli/src/commands/status.js.map +1 -1
- package/dist/cli/src/commands/up.js +40 -3
- package/dist/cli/src/commands/up.js.map +1 -1
- package/dist/cli/src/commands/upgrade.js +22 -6
- package/dist/cli/src/commands/upgrade.js.map +1 -1
- package/dist/cli/src/config.js +20 -8
- package/dist/cli/src/config.js.map +1 -1
- package/dist/cli/src/cpu-baseline.js +97 -0
- package/dist/cli/src/cpu-baseline.js.map +1 -0
- package/dist/cli/src/install.js +94 -13
- package/dist/cli/src/install.js.map +1 -1
- package/dist/cli/src/kept-releases.js +59 -0
- package/dist/cli/src/kept-releases.js.map +1 -0
- package/dist/cli/src/kube/api.js +9 -2
- package/dist/cli/src/kube/api.js.map +1 -1
- package/dist/cli/src/kube/apply.js +1 -1
- package/dist/cli/src/kube/apply.js.map +1 -1
- package/dist/cli/src/kube/rollout.js +79 -12
- package/dist/cli/src/kube/rollout.js.map +1 -1
- package/dist/cli/src/lake.js +124 -0
- package/dist/cli/src/lake.js.map +1 -0
- package/dist/cli/src/manifests/admission.js +183 -24
- package/dist/cli/src/manifests/admission.js.map +1 -1
- package/dist/cli/src/manifests/alasio.js +198 -27
- package/dist/cli/src/manifests/alasio.js.map +1 -1
- package/dist/cli/src/manifests/branch.js +2 -1
- package/dist/cli/src/manifests/branch.js.map +1 -1
- package/dist/cli/src/manifests/collector.js +229 -122
- package/dist/cli/src/manifests/collector.js.map +1 -1
- package/dist/cli/src/manifests/common.js +23 -8
- package/dist/cli/src/manifests/common.js.map +1 -1
- package/dist/cli/src/manifests/config.js +254 -21
- package/dist/cli/src/manifests/config.js.map +1 -1
- package/dist/cli/src/manifests/grafana.js +18 -6
- package/dist/cli/src/manifests/grafana.js.map +1 -1
- package/dist/cli/src/manifests/index.js +15 -6
- package/dist/cli/src/manifests/index.js.map +1 -1
- package/dist/cli/src/manifests/juicefs-csi.js +7 -6
- package/dist/cli/src/manifests/juicefs-csi.js.map +1 -1
- package/dist/cli/src/manifests/keda.js +2 -1
- package/dist/cli/src/manifests/keda.js.map +1 -1
- package/dist/cli/src/manifests/lake.js +340 -73
- package/dist/cli/src/manifests/lake.js.map +1 -1
- package/dist/cli/src/manifests/log.js +298 -0
- package/dist/cli/src/manifests/log.js.map +1 -0
- package/dist/cli/src/manifests/neon.js +282 -66
- package/dist/cli/src/manifests/neon.js.map +1 -1
- package/dist/cli/src/manifests/network-policies.js +211 -45
- package/dist/cli/src/manifests/network-policies.js.map +1 -1
- package/dist/cli/src/manifests/node-agent.js +116 -0
- package/dist/cli/src/manifests/node-agent.js.map +1 -0
- package/dist/cli/src/manifests/object-store.js +42 -10
- package/dist/cli/src/manifests/object-store.js.map +1 -1
- package/dist/cli/src/manifests/priorities.js +1 -0
- package/dist/cli/src/manifests/priorities.js.map +1 -1
- package/dist/cli/src/manifests/scalers.js.map +1 -1
- package/dist/cli/src/manifests/valkey.js +1 -1
- package/dist/cli/src/manifests/webhook.js +102 -0
- package/dist/cli/src/manifests/webhook.js.map +1 -0
- package/dist/cli/src/neon-storage.js +115 -0
- package/dist/cli/src/neon-storage.js.map +1 -0
- package/dist/cli/src/release.js +9 -6
- package/dist/cli/src/release.js.map +1 -1
- package/dist/cli/src/secrets.js +45 -4
- package/dist/cli/src/secrets.js.map +1 -1
- package/dist/cli/src/target.js +1 -1
- package/dist/cli/src/target.js.map +1 -1
- package/dist/cli/src/telegram.js +56 -10
- package/dist/cli/src/telegram.js.map +1 -1
- package/dist/cli/src/webhook.js +182 -0
- package/dist/cli/src/webhook.js.map +1 -0
- package/dist/cluster/node/pins.json +11 -2
- package/dist/neon/control/branches.js +146 -22
- package/dist/neon/control/branches.js.map +1 -1
- package/dist/neon/control/endpoints.js +117 -0
- package/dist/neon/control/endpoints.js.map +1 -0
- package/dist/neon/control/jwt.js +16 -3
- package/dist/neon/control/jwt.js.map +1 -1
- package/dist/neon/control/scram.js +13 -1
- package/dist/neon/control/scram.js.map +1 -1
- package/dist/neon/control/secrets.js +88 -14
- package/dist/neon/control/secrets.js.map +1 -1
- package/dist/src/kube/config.js +27 -3
- package/dist/src/kube/config.js.map +1 -1
- package/dist/src/kube/sandbox-shape.js +49 -3
- package/dist/src/kube/sandbox-shape.js.map +1 -1
- package/dist/src/node-agent/names.js +140 -0
- package/dist/src/node-agent/names.js.map +1 -0
- package/dist/src/telegram/deliveries.js +40 -0
- package/dist/src/telegram/deliveries.js.map +1 -0
- package/package.json +2 -2
- package/dist/cluster/node/config-v3.toml.tmpl +0 -12
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { computeId, MAIN } from "../../../neon/control/branches.js";
|
|
1
|
+
import { COMPUTE_CTL_PORT, COMPUTE_PORT, computeId, MAIN, REPLICA_SUFFIX, replicaId } from "../../../neon/control/branches.js";
|
|
2
|
+
import { SUSPEND_AFTER } from "../../../neon/control/endpoints.js";
|
|
3
|
+
import { DATABASE, PAGESERVER_ZONE, ROLE } from "../../../neon/control/secrets.js";
|
|
2
4
|
import { imageReference } from "../images.js";
|
|
3
|
-
import {
|
|
5
|
+
import { computeRestartPolicy } from "./admission.js";
|
|
6
|
+
import { claimSpec, collectorRuns, databaseSecret, given, helperResources, imagePullSecrets, lakeRuns, MAIN_ENVIRONMENT, NAMESPACE, neonName, ONE, otelEnv, RELEASE, replicasOf, restrictedContainer, s3Endpoint, sandboxSnapshots, script, selectorLabels, stackLabels, stackPodSpec, waitFor, waitForObjectStore, } from "./common.js";
|
|
7
|
+
import { durationSeconds } from "./config.js";
|
|
4
8
|
import { replicaBudget, spread } from "./disruption.js";
|
|
5
9
|
import { VOLUME_DRIVER } from "./juicefs-csi.js";
|
|
6
10
|
import { priority } from "./priorities.js";
|
|
@@ -20,9 +24,12 @@ function secretVariable(secret, key) {
|
|
|
20
24
|
return { name: key, valueFrom: { secretKeyRef: { name: secret, key } } };
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
|
-
* The stack's secrets, made once and kept, and each service's rendered from them,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
27
|
+
* The stack's secrets, made once and kept, and each service's rendered from them, by
|
|
28
|
+
* neon/control/kube-setup.ts: the log's and the webhook intake's; and where Neon runs,
|
|
29
|
+
* Neon's, the object store's, workspace storage's when it is on, and the snapshots
|
|
30
|
+
* bucket's and the node agents' when sandboxes are snapshotted: a Job, which applying
|
|
31
|
+
* alasio runs to completion before anything else of alasio starts (../kube/apply.ts), with
|
|
32
|
+
* Neon or without it.
|
|
26
33
|
*/
|
|
27
34
|
function setup(config) {
|
|
28
35
|
const name = neonName("setup");
|
|
@@ -64,15 +71,20 @@ function setup(config) {
|
|
|
64
71
|
env: [
|
|
65
72
|
{ name: "NAMESPACE", valueFrom: { fieldRef: { fieldPath: "metadata.namespace" } } },
|
|
66
73
|
{ name: "SECRET_PREFIX", value: RELEASE },
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
...(config.neon.enabled
|
|
75
|
+
? [
|
|
76
|
+
{ name: "NEON_BROKER_URL", value: `http://${neonName("storage-broker")}:50051` },
|
|
77
|
+
{ name: "NEON_CONTROLLER_URL", value: `http://${neonName("storage-controller")}:1234` },
|
|
78
|
+
{ name: "NEON_PROXY_HOST", value: PROXY_HOST },
|
|
79
|
+
{ name: "NEON_CONTROLLER_DB_HOST", value: neonName("controller-db") },
|
|
80
|
+
]
|
|
81
|
+
: []),
|
|
72
82
|
{ name: "S3_ENDPOINT", value: s3Endpoint(config) },
|
|
73
83
|
{ name: "S3_REGION", value: objectStore.external.region },
|
|
74
84
|
{ name: "S3_BUCKET_NEON", value: objectStore.buckets.neon },
|
|
75
85
|
{ name: "S3_BUCKET_LAKE", value: objectStore.buckets.lake },
|
|
86
|
+
{ name: "S3_BUCKET_BACKUPS", value: objectStore.buckets.backups },
|
|
87
|
+
{ name: "WEBHOOK_ADDRESS", value: config.webhook.publicAddress },
|
|
76
88
|
...(workspaceStorage.enabled
|
|
77
89
|
? [
|
|
78
90
|
{ name: "WORKSPACES_NAME", value: workspaceStorage.name },
|
|
@@ -83,10 +95,17 @@ function setup(config) {
|
|
|
83
95
|
{ name: "WORKSPACES_SECRET_LABELS", value: JSON.stringify(VOLUME_DRIVER) },
|
|
84
96
|
]
|
|
85
97
|
: []),
|
|
98
|
+
...(sandboxSnapshots(config) ? [{ name: "SNAPSHOTS_BUCKET", value: objectStore.buckets.snapshots }] : []),
|
|
86
99
|
...(objectStore.bundled.enabled ? [] : [
|
|
87
100
|
{ name: "S3_EXTERNAL", value: "1" },
|
|
88
101
|
{ name: "S3_ACCESS_KEY", valueFrom: { secretKeyRef: { name: objectStore.external.existingSecret, key: "accessKey" } } },
|
|
89
102
|
{ name: "S3_SECRET_KEY", valueFrom: { secretKeyRef: { name: objectStore.external.existingSecret, key: "secretKey" } } },
|
|
103
|
+
...(sandboxSnapshots(config)
|
|
104
|
+
? [
|
|
105
|
+
{ name: "S3_SNAPSHOTS_ACCESS_KEY", valueFrom: { secretKeyRef: { name: objectStore.external.snapshotsSecret, key: "accessKey" } } },
|
|
106
|
+
{ name: "S3_SNAPSHOTS_SECRET_KEY", valueFrom: { secretKeyRef: { name: objectStore.external.snapshotsSecret, key: "secretKey" } } },
|
|
107
|
+
]
|
|
108
|
+
: []),
|
|
90
109
|
]),
|
|
91
110
|
],
|
|
92
111
|
securityContext: restrictedContainer(),
|
|
@@ -113,7 +132,7 @@ function controllerDb(config) {
|
|
|
113
132
|
template: {
|
|
114
133
|
metadata: { labels: stackLabels(component) },
|
|
115
134
|
spec: {
|
|
116
|
-
...priority("
|
|
135
|
+
...priority("bound"),
|
|
117
136
|
...stackPodSpec(config),
|
|
118
137
|
containers: [{
|
|
119
138
|
name: "postgres",
|
|
@@ -253,7 +272,7 @@ function safekeepers(config) {
|
|
|
253
272
|
template: {
|
|
254
273
|
metadata: { labels: stackLabels(component) },
|
|
255
274
|
spec: {
|
|
256
|
-
...priority("
|
|
275
|
+
...priority("bound"),
|
|
257
276
|
...stackPodSpec(config),
|
|
258
277
|
terminationGracePeriodSeconds: 30,
|
|
259
278
|
...given("initContainers", waitForObjectStore(config)),
|
|
@@ -315,35 +334,80 @@ function safekeepers(config) {
|
|
|
315
334
|
};
|
|
316
335
|
return [headless, statefulSet, ...replicaBudget(component, NAMESPACE, config.neon.safekeepers.scaling), verticalPodAutoscaler(statefulSet, "Recommended")];
|
|
317
336
|
}
|
|
337
|
+
/** The name of the pageserver of ordinal `index`, its Service's, which it registers itself at: its pod's. */
|
|
338
|
+
export const pageserverName = (index) => `${neonName("pageserver")}-${index}`;
|
|
339
|
+
/** The pageservers' governing Service, headless, which their StatefulSet names: of no one pageserver, as pageserverName is. */
|
|
340
|
+
export const PAGESERVERS_NAME = neonName("pageservers");
|
|
318
341
|
/**
|
|
319
|
-
* The
|
|
320
|
-
* cache.
|
|
342
|
+
* The pageservers: pages served from layers each keeps in the object store, its volume a
|
|
343
|
+
* cache, as many as their scale fixes. Each registers itself with the storage controller
|
|
344
|
+
* as its ordinal plus one, at a Service of its own alone (pageserverName), which resolves
|
|
345
|
+
* before it is ready, as registration needs; the controller places the tenant's shards on
|
|
346
|
+
* them, and computes reach each shard's pageserver at that name (neon-control's specs),
|
|
347
|
+
* never through one address that would balance them. They start at once, none waiting for
|
|
348
|
+
* another to be ready, which waits for the storage controller: so a restore that starts what
|
|
349
|
+
* its volumes hold to a node before the rest (../commands/restore-filer-store.ts) finds
|
|
350
|
+
* every pageserver's pod made. Their StatefulSet's governing Service (PAGESERVERS_NAME) is
|
|
351
|
+
* headless, giving each pod its DNS identity, which no address uses: a pod's own Service
|
|
352
|
+
* keeps its address while it is not ready, as registration needs, and alone resolves to it.
|
|
321
353
|
*/
|
|
322
354
|
function pageserver(config) {
|
|
323
355
|
const name = neonName("pageserver");
|
|
324
356
|
const component = "neon-pageserver";
|
|
325
357
|
const image = imageReference(config.neon.image);
|
|
358
|
+
const { scaling, storage, resources } = config.neon.pageserver;
|
|
359
|
+
const services = Array.from({ length: scaling.min }, (_, index) => ({
|
|
360
|
+
apiVersion: "v1",
|
|
361
|
+
kind: "Service",
|
|
362
|
+
metadata: { name: pageserverName(index), namespace: NAMESPACE, labels: stackLabels(component) },
|
|
363
|
+
spec: {
|
|
364
|
+
selector: { ...selectorLabels(component), "statefulset.kubernetes.io/pod-name": `${name}-${index}` },
|
|
365
|
+
publishNotReadyAddresses: true,
|
|
366
|
+
ports: [{ name: "pg", port: 6400 }, { name: "http", port: 9898 }],
|
|
367
|
+
},
|
|
368
|
+
}));
|
|
369
|
+
const governing = {
|
|
370
|
+
apiVersion: "v1",
|
|
371
|
+
kind: "Service",
|
|
372
|
+
metadata: { name: PAGESERVERS_NAME, namespace: NAMESPACE, labels: stackLabels(component) },
|
|
373
|
+
spec: { clusterIP: "None", publishNotReadyAddresses: true, selector: selectorLabels(component), ports: [{ name: "pg", port: 6400 }, { name: "http", port: 9898 }] },
|
|
374
|
+
};
|
|
326
375
|
const statefulSet = {
|
|
327
376
|
apiVersion: "apps/v1",
|
|
328
377
|
kind: "StatefulSet",
|
|
329
378
|
metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
|
|
330
379
|
spec: {
|
|
331
|
-
serviceName:
|
|
332
|
-
...replicasOf(
|
|
380
|
+
serviceName: PAGESERVERS_NAME,
|
|
381
|
+
...replicasOf(scaling),
|
|
382
|
+
podManagementPolicy: "Parallel",
|
|
333
383
|
selector: { matchLabels: selectorLabels(component) },
|
|
334
384
|
template: {
|
|
335
385
|
metadata: { labels: stackLabels(component) },
|
|
336
386
|
spec: {
|
|
337
|
-
...priority("
|
|
387
|
+
...priority("bound"),
|
|
338
388
|
...stackPodSpec(config),
|
|
389
|
+
...spread(component, scaling, true),
|
|
339
390
|
terminationGracePeriodSeconds: 30,
|
|
340
391
|
initContainers: [
|
|
341
392
|
{
|
|
342
393
|
name: "config",
|
|
343
394
|
image,
|
|
344
395
|
imagePullPolicy: "IfNotPresent",
|
|
345
|
-
// Its configuration,
|
|
346
|
-
|
|
396
|
+
// Its configuration, the stack's setup's, into its working directory, and its
|
|
397
|
+
// identity, its own: its ordinal plus one, at its own Service's name, its pod's.
|
|
398
|
+
// The ordinal is its pod's name's last part, as the StatefulSet names its pods,
|
|
399
|
+
// on any Kubernetes (the pod-index label is 1.28's).
|
|
400
|
+
command: [
|
|
401
|
+
"/bin/sh",
|
|
402
|
+
"-c",
|
|
403
|
+
[
|
|
404
|
+
'POD_INDEX="${POD_NAME##*-}" &&',
|
|
405
|
+
"cp /config/pageserver.toml /config/auth_public_key.pem /data/.neon/ &&",
|
|
406
|
+
'echo "id=$((POD_INDEX + 1))" > /data/.neon/identity.toml &&',
|
|
407
|
+
`printf '{"host":"%s","port":6400,"http_host":"%s","http_port":9898,"availability_zone_id":"${PAGESERVER_ZONE}"}\\n' "$POD_NAME" "$POD_NAME" > /data/.neon/metadata.json`,
|
|
408
|
+
].join(" "),
|
|
409
|
+
],
|
|
410
|
+
env: [{ name: "POD_NAME", valueFrom: { fieldRef: { fieldPath: "metadata.name" } } }],
|
|
347
411
|
securityContext: restrictedContainer(),
|
|
348
412
|
...helperResources(),
|
|
349
413
|
volumeMounts: [{ name: "data", mountPath: "/data/.neon" }, { name: "config", mountPath: "/config", readOnly: true }],
|
|
@@ -360,53 +424,83 @@ function pageserver(config) {
|
|
|
360
424
|
ports: [{ name: "pg", containerPort: 6400 }, { name: "http", containerPort: 9898 }],
|
|
361
425
|
readinessProbe: { httpGet: { path: "/v1/status", port: "http" }, periodSeconds: 5 },
|
|
362
426
|
securityContext: restrictedContainer(),
|
|
363
|
-
resources
|
|
427
|
+
resources,
|
|
364
428
|
volumeMounts: [{ name: "data", mountPath: "/data/.neon" }],
|
|
365
429
|
}],
|
|
366
430
|
volumes: [{
|
|
367
431
|
name: "config",
|
|
368
|
-
secret: {
|
|
369
|
-
secretName: name,
|
|
370
|
-
items: ["pageserver.toml", "identity.toml", "metadata.json", "auth_public_key.pem"].map((key) => ({ key, path: key })),
|
|
371
|
-
},
|
|
432
|
+
secret: { secretName: name, items: ["pageserver.toml", "auth_public_key.pem"].map((key) => ({ key, path: key })) },
|
|
372
433
|
}],
|
|
373
434
|
},
|
|
374
435
|
},
|
|
375
|
-
volumeClaimTemplates: [{ metadata: { name: "data" }, spec: claimSpec(
|
|
436
|
+
volumeClaimTemplates: [{ metadata: { name: "data" }, spec: claimSpec(storage) }],
|
|
376
437
|
},
|
|
377
438
|
};
|
|
378
|
-
return [
|
|
439
|
+
return [governing, ...services, statefulSet, ...replicaBudget(component, NAMESPACE, scaling), verticalPodAutoscaler(statefulSet, "Recommended")];
|
|
440
|
+
}
|
|
441
|
+
/** Where every client reaches Neon's proxy, from main's namespace or a branch environment's. */
|
|
442
|
+
export const PROXY_HOST = `${neonName("proxy")}.${NAMESPACE}.svc`;
|
|
443
|
+
/** The port the proxy takes clients on, Postgres's own. */
|
|
444
|
+
export const PROXY_PORT = 5432;
|
|
445
|
+
/**
|
|
446
|
+
* What neon-control may do to the compute of `namespace`, main's or a branch
|
|
447
|
+
* environment's, and to its read replica: read its Deployment, and restart it, to start on
|
|
448
|
+
* a spec anew, and scale it, waking it and putting it to sleep, and list its pods, to see
|
|
449
|
+
* one put to sleep gone before it wakes it; a Role there, bound to neon-control's
|
|
450
|
+
* ServiceAccount in main's. Of a Deployment it patches, it may change the restart's
|
|
451
|
+
* annotation alone (./admission.ts's computeRestartPolicy).
|
|
452
|
+
*/
|
|
453
|
+
export function computeScaling(namespace) {
|
|
454
|
+
const name = neonName("control");
|
|
455
|
+
const metadata = { name, namespace, labels: stackLabels("neon-control") };
|
|
456
|
+
const role = {
|
|
457
|
+
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
458
|
+
kind: "Role",
|
|
459
|
+
metadata,
|
|
460
|
+
rules: [
|
|
461
|
+
{ apiGroups: ["apps"], resources: ["deployments"], resourceNames: [neonName("compute"), REPLICA_NAME], verbs: ["get", "patch"] },
|
|
462
|
+
{ apiGroups: ["apps"], resources: ["deployments/scale"], resourceNames: [neonName("compute"), REPLICA_NAME], verbs: ["patch"] },
|
|
463
|
+
// A list is of a namespace's pods, as no name narrows it.
|
|
464
|
+
{ apiGroups: [""], resources: ["pods"], verbs: ["list"] },
|
|
465
|
+
],
|
|
466
|
+
};
|
|
467
|
+
const binding = {
|
|
468
|
+
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
469
|
+
kind: "RoleBinding",
|
|
470
|
+
metadata,
|
|
471
|
+
subjects: [{ kind: "ServiceAccount", name, namespace: NAMESPACE }],
|
|
472
|
+
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name },
|
|
473
|
+
};
|
|
474
|
+
return [role, binding];
|
|
379
475
|
}
|
|
380
476
|
/**
|
|
381
477
|
* neon-control: it bootstraps the tenant and main's timeline, makes and deletes branches,
|
|
382
|
-
* serves each branch's compute its spec, answers the storage controller's hooks
|
|
383
|
-
*
|
|
384
|
-
* its
|
|
478
|
+
* serves each branch's compute its spec, answers the storage controller's hooks and Neon's
|
|
479
|
+
* proxy, wakes computes and puts them to sleep, and repairs a safekeeper that lost its
|
|
480
|
+
* disk. Its record of the branches is in the storage controller's Postgres, so any of its
|
|
481
|
+
* replicas serves.
|
|
385
482
|
*/
|
|
386
483
|
function control(config) {
|
|
387
484
|
const name = neonName("control");
|
|
388
485
|
const component = "neon-control";
|
|
486
|
+
const { scaling, resources } = config.neon.control;
|
|
389
487
|
const safekeeper = neonName("safekeeper");
|
|
390
488
|
const safekeeperHosts = Array.from({ length: config.neon.safekeepers.scaling.min }, (_, index) => `${safekeeper}-${index}.${safekeeper}.${NAMESPACE}.svc.cluster.local`);
|
|
391
|
-
const
|
|
392
|
-
apiVersion: "v1",
|
|
393
|
-
kind: "PersistentVolumeClaim",
|
|
394
|
-
metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
|
|
395
|
-
spec: claimSpec(config.neon.control.storage),
|
|
396
|
-
};
|
|
489
|
+
const serviceAccount = { apiVersion: "v1", kind: "ServiceAccount", metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) } };
|
|
397
490
|
const deployment = {
|
|
398
491
|
apiVersion: "apps/v1",
|
|
399
492
|
kind: "Deployment",
|
|
400
493
|
metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
|
|
401
494
|
spec: {
|
|
402
|
-
...replicasOf(
|
|
403
|
-
strategy: { type: "Recreate" },
|
|
495
|
+
...replicasOf(scaling),
|
|
404
496
|
selector: { matchLabels: selectorLabels(component) },
|
|
405
497
|
template: {
|
|
406
498
|
metadata: { labels: stackLabels(component) },
|
|
407
499
|
spec: {
|
|
500
|
+
serviceAccountName: name,
|
|
408
501
|
...priority("floor"),
|
|
409
502
|
...stackPodSpec(config),
|
|
503
|
+
...spread(component, scaling),
|
|
410
504
|
containers: [{
|
|
411
505
|
name: "neon-control",
|
|
412
506
|
image: imageReference(config.images.alasio),
|
|
@@ -415,21 +509,22 @@ function control(config) {
|
|
|
415
509
|
env: [
|
|
416
510
|
{ name: "CONTROLLER_URL", value: `http://${neonName("storage-controller")}:1234` },
|
|
417
511
|
{ name: "NEON_SAFEKEEPER_HOSTS", value: safekeeperHosts.join(",") },
|
|
418
|
-
|
|
419
|
-
{ name: "
|
|
512
|
+
{ name: "NEON_PAGESERVERS", value: String(config.neon.pageserver.scaling.min) },
|
|
513
|
+
{ name: "NEON_SHARDS", value: String(config.neon.shards) },
|
|
514
|
+
{ name: "NEON_CONTROLLER_DB_HOST", value: neonName("controller-db") },
|
|
515
|
+
{ name: "NAMESPACE", valueFrom: { fieldRef: { fieldPath: "metadata.namespace" } } },
|
|
516
|
+
{ name: "NEON_COMPUTE_NAME", value: neonName("compute") },
|
|
420
517
|
],
|
|
421
518
|
ports: [{ name: "http", containerPort: 8080 }],
|
|
422
519
|
readinessProbe: { httpGet: { path: "/healthz", port: "http" }, periodSeconds: 3 },
|
|
423
520
|
securityContext: { ...restrictedContainer(), readOnlyRootFilesystem: true },
|
|
424
|
-
resources
|
|
521
|
+
resources,
|
|
425
522
|
volumeMounts: [
|
|
426
|
-
{ name: "state", mountPath: "/state" },
|
|
427
523
|
{ name: "root", mountPath: "/secrets", readOnly: true },
|
|
428
524
|
{ name: "keys", mountPath: "/keys", readOnly: true },
|
|
429
525
|
],
|
|
430
526
|
}],
|
|
431
527
|
volumes: [
|
|
432
|
-
{ name: "state", persistentVolumeClaim: { claimName: name } },
|
|
433
528
|
{
|
|
434
529
|
name: "root",
|
|
435
530
|
secret: {
|
|
@@ -444,7 +539,76 @@ function control(config) {
|
|
|
444
539
|
},
|
|
445
540
|
},
|
|
446
541
|
};
|
|
447
|
-
return [
|
|
542
|
+
return [
|
|
543
|
+
service(name, component, [{ name: "http", port: 8080 }]),
|
|
544
|
+
serviceAccount,
|
|
545
|
+
...computeScaling(NAMESPACE),
|
|
546
|
+
...computeRestartPolicy(),
|
|
547
|
+
deployment,
|
|
548
|
+
...replicaBudget(component, NAMESPACE, scaling),
|
|
549
|
+
verticalPodAutoscaler(deployment, "Recommended"),
|
|
550
|
+
];
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Neon's proxy, from Neon's image: every client of a compute, main's or a branch
|
|
554
|
+
* environment's, connects to it, naming the compute by `options=endpoint=<id>` (the
|
|
555
|
+
* compute's id, as its spec is served under), without TLS, which would turn that option
|
|
556
|
+
* off. It asks neon-control, its control plane, for the role's verifier, authenticates the
|
|
557
|
+
* client against it, has neon-control wake the compute if it sleeps, and then passes the
|
|
558
|
+
* session through, whole, so session state such as advisory locks holds. It keeps where
|
|
559
|
+
* each compute is for four minutes, and asks again when a connection there fails, as one
|
|
560
|
+
* to a compute that has gone to sleep since does. It cancels no query: that needs a Redis
|
|
561
|
+
* of its own, and alasio cancels none.
|
|
562
|
+
*/
|
|
563
|
+
function proxy(config) {
|
|
564
|
+
const name = neonName("proxy");
|
|
565
|
+
const component = "neon-proxy";
|
|
566
|
+
const { scaling, resources } = config.neon.proxy;
|
|
567
|
+
const deployment = {
|
|
568
|
+
apiVersion: "apps/v1",
|
|
569
|
+
kind: "Deployment",
|
|
570
|
+
metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
|
|
571
|
+
spec: {
|
|
572
|
+
...replicasOf(scaling),
|
|
573
|
+
selector: { matchLabels: selectorLabels(component) },
|
|
574
|
+
template: {
|
|
575
|
+
metadata: { labels: stackLabels(component) },
|
|
576
|
+
spec: {
|
|
577
|
+
...priority("floor"),
|
|
578
|
+
...stackPodSpec(config),
|
|
579
|
+
...spread(component, scaling),
|
|
580
|
+
containers: [{
|
|
581
|
+
name: "proxy",
|
|
582
|
+
image: imageReference(config.neon.image),
|
|
583
|
+
imagePullPolicy: "IfNotPresent",
|
|
584
|
+
command: [
|
|
585
|
+
"proxy",
|
|
586
|
+
"--proxy", `0.0.0.0:${PROXY_PORT}`,
|
|
587
|
+
"--http", "0.0.0.0:7001",
|
|
588
|
+
"--auth-backend", "control-plane",
|
|
589
|
+
"--auth-endpoint", `http://${neonName("control")}:8080`,
|
|
590
|
+
"--wake-compute-cache", "size=4000,ttl=4m",
|
|
591
|
+
// A role's secret is kept ten seconds, not its four minutes: no Redis tells it of a
|
|
592
|
+
// branch deleted and made again under its name, with new passwords.
|
|
593
|
+
"--project-info-cache", "size=10000,ttl=10s,max_roles=10,gc_interval=60m",
|
|
594
|
+
],
|
|
595
|
+
// Its token for neon-control, of the scope that answers it alone.
|
|
596
|
+
env: [secretVariable(name, "NEON_PROXY_TO_CONTROLPLANE_TOKEN")],
|
|
597
|
+
ports: [{ name: "postgres", containerPort: PROXY_PORT }, { name: "http", containerPort: 7001 }],
|
|
598
|
+
readinessProbe: { tcpSocket: { port: "postgres" }, periodSeconds: 5 },
|
|
599
|
+
securityContext: { ...restrictedContainer(), readOnlyRootFilesystem: true },
|
|
600
|
+
resources,
|
|
601
|
+
}],
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
return [
|
|
607
|
+
service(name, component, [{ name: "postgres", port: PROXY_PORT }, { name: "http", port: 7001 }]),
|
|
608
|
+
deployment,
|
|
609
|
+
...replicaBudget(component, NAMESPACE, scaling),
|
|
610
|
+
verticalPodAutoscaler(deployment, "InPlace"),
|
|
611
|
+
];
|
|
448
612
|
}
|
|
449
613
|
/**
|
|
450
614
|
* The compute: Postgres on alasio's timeline, one primary (never two on one timeline),
|
|
@@ -453,24 +617,49 @@ function control(config) {
|
|
|
453
617
|
* scrapes, and exports compute_ctl's traces to the collector, when it runs. A branch
|
|
454
618
|
* environment's is on the branch's timeline, in its namespace, reaching the stack's
|
|
455
619
|
* storage and neon-control in main's.
|
|
620
|
+
*
|
|
621
|
+
* neon-control wakes it and puts it to sleep, scaling its Deployment between one replica
|
|
622
|
+
* and none: so applying alasio gives it none, as it would take the count back, and a
|
|
623
|
+
* compute made anew starts at one. Its Service takes connections before its pod is ready,
|
|
624
|
+
* so a wake is answered as soon as Postgres takes them.
|
|
625
|
+
*
|
|
626
|
+
* Beside it, where the lake runs, its read replica (REPLICA_NAME), which the lake loads
|
|
627
|
+
* from: a compute of its own, on the same timeline, streaming its WAL from the
|
|
628
|
+
* safekeepers, as neon-control specifies it, smaller, woken and put to sleep as the
|
|
629
|
+
* primary is, of the class of what sleeps, as it serves the loader alone.
|
|
456
630
|
*/
|
|
457
631
|
export function compute(config, environment = MAIN_ENVIRONMENT) {
|
|
458
|
-
|
|
459
|
-
|
|
632
|
+
return [...computeObjects(config, environment, false), ...(lakeRuns(config) ? computeObjects(config, environment, true) : [])];
|
|
633
|
+
}
|
|
634
|
+
/** The name of a read replica's Deployment and Service, beside its primary's in each environment. */
|
|
635
|
+
export const REPLICA_NAME = `${neonName("compute")}${REPLICA_SUFFIX}`;
|
|
636
|
+
/** The objects of the environment's primary compute, or with `replica` its read replica's. */
|
|
637
|
+
function computeObjects(config, environment, replica) {
|
|
638
|
+
const name = replica ? REPLICA_NAME : neonName("compute");
|
|
639
|
+
const component = replica ? "neon-compute-ro" : "neon-compute";
|
|
460
640
|
const control = `http://${neonName("control")}.${NAMESPACE}.svc:8080`;
|
|
641
|
+
const { suspendAfter } = config.neon.compute;
|
|
642
|
+
const resources = replica ? config.neon.readReplica.resources : config.neon.compute.resources;
|
|
643
|
+
const branch = environment.branch ?? MAIN;
|
|
461
644
|
const deployment = {
|
|
462
645
|
apiVersion: "apps/v1",
|
|
463
646
|
kind: "Deployment",
|
|
464
|
-
metadata: {
|
|
647
|
+
metadata: {
|
|
648
|
+
name,
|
|
649
|
+
namespace: environment.namespace,
|
|
650
|
+
labels: stackLabels(component),
|
|
651
|
+
// A valid duration, as the configuration checks.
|
|
652
|
+
annotations: { [SUSPEND_AFTER]: String(durationSeconds(suspendAfter)) },
|
|
653
|
+
},
|
|
465
654
|
spec: {
|
|
466
|
-
...replicasOf(ONE),
|
|
467
655
|
strategy: { type: "Recreate" },
|
|
468
656
|
selector: { matchLabels: selectorLabels(component) },
|
|
469
657
|
template: {
|
|
470
658
|
metadata: { labels: stackLabels(component) },
|
|
471
659
|
spec: {
|
|
472
|
-
// Main's is alasio's database, which every turn needs; a branch's
|
|
473
|
-
|
|
660
|
+
// Main's primary is alasio's database, which every turn needs; a branch's, and every
|
|
661
|
+
// read replica, serve what may wait.
|
|
662
|
+
...priority(environment.branch === null && !replica ? "floor" : "component"),
|
|
474
663
|
...imagePullSecrets(config),
|
|
475
664
|
// The compute image runs as its own postgres user.
|
|
476
665
|
securityContext: { fsGroup: config.neon.runAsGroup, seccompProfile: { type: "RuntimeDefault" } },
|
|
@@ -486,24 +675,25 @@ export function compute(config, environment = MAIN_ENVIRONMENT) {
|
|
|
486
675
|
"--pgdata",
|
|
487
676
|
"/var/db/postgres/compute",
|
|
488
677
|
"--connstr",
|
|
489
|
-
|
|
678
|
+
`postgresql://cloud_admin@localhost:${COMPUTE_PORT}/postgres`,
|
|
490
679
|
"--pgbin",
|
|
491
680
|
"/usr/local/bin/postgres",
|
|
492
681
|
"--compute-id",
|
|
493
|
-
|
|
682
|
+
replica ? replicaId(branch) : computeId(branch),
|
|
494
683
|
"--control-plane-uri",
|
|
495
684
|
control,
|
|
496
685
|
],
|
|
497
686
|
env: [
|
|
498
|
-
|
|
687
|
+
// The primary's token, which fetches its replica's spec too.
|
|
688
|
+
secretVariable(neonName("compute"), "NEON_CONTROL_PLANE_TOKEN"),
|
|
499
689
|
...(collectorRuns(config)
|
|
500
690
|
? [{ name: "OTEL_SERVICE_NAME", value: "compute_ctl" }, ...otelEnv(config, environment)]
|
|
501
691
|
: [{ name: "OTEL_SDK_DISABLED", value: "true" }]),
|
|
502
692
|
],
|
|
503
|
-
ports: [{ name: "postgres", containerPort:
|
|
504
|
-
readinessProbe: { exec: { command: ["pg_isready", "-h", "127.0.0.1", "-p",
|
|
693
|
+
ports: [{ name: "postgres", containerPort: COMPUTE_PORT }, { name: "http", containerPort: COMPUTE_CTL_PORT }],
|
|
694
|
+
readinessProbe: { exec: { command: ["pg_isready", "-h", "127.0.0.1", "-p", String(COMPUTE_PORT), "-U", "cloud_admin", "-d", "postgres"] }, periodSeconds: 5 },
|
|
505
695
|
securityContext: restrictedContainer(),
|
|
506
|
-
resources
|
|
696
|
+
resources,
|
|
507
697
|
volumeMounts: [{ name: "pgdata", mountPath: "/var/db/postgres" }],
|
|
508
698
|
}],
|
|
509
699
|
volumes: [{ name: "pgdata", emptyDir: {} }],
|
|
@@ -511,7 +701,13 @@ export function compute(config, environment = MAIN_ENVIRONMENT) {
|
|
|
511
701
|
},
|
|
512
702
|
},
|
|
513
703
|
};
|
|
514
|
-
|
|
704
|
+
const service = {
|
|
705
|
+
apiVersion: "v1",
|
|
706
|
+
kind: "Service",
|
|
707
|
+
metadata: { name, namespace: environment.namespace, labels: stackLabels(component) },
|
|
708
|
+
spec: { selector: selectorLabels(component), publishNotReadyAddresses: true, ports: [{ name: "postgres", port: COMPUTE_PORT }, { name: "http", port: COMPUTE_CTL_PORT }] },
|
|
709
|
+
};
|
|
710
|
+
return [service, deployment, verticalPodAutoscaler(deployment, "Recommended")];
|
|
515
711
|
}
|
|
516
712
|
/**
|
|
517
713
|
* A logical dump of alasio's database, restorable into any Postgres whatever becomes of
|
|
@@ -529,12 +725,18 @@ function backup(config) {
|
|
|
529
725
|
command: [
|
|
530
726
|
"/bin/sh",
|
|
531
727
|
"-c",
|
|
532
|
-
// A new pod's NetworkPolicy may apply a moment after it starts, so the
|
|
728
|
+
// A new pod's NetworkPolicy may apply a moment after it starts, so the proxy is
|
|
533
729
|
// waited for rather than given up on at the first refusal.
|
|
534
|
-
|
|
535
|
-
|
|
730
|
+
`until pg_isready --quiet; do sleep 2; done && pg_dump --username=${ROLE} --dbname=${DATABASE} --format=custom --file=/backup/alasio.dump`,
|
|
731
|
+
],
|
|
732
|
+
env: [
|
|
733
|
+
{ name: "PGHOST", value: PROXY_HOST },
|
|
734
|
+
{ name: "PGPORT", value: String(PROXY_PORT) },
|
|
735
|
+
// Main's compute, through the proxy, which wakes it if it sleeps.
|
|
736
|
+
{ name: "PGOPTIONS", value: `endpoint=${computeId(MAIN)}` },
|
|
737
|
+
{ name: "PGCONNECT_TIMEOUT", value: "60" },
|
|
738
|
+
{ name: "PGPASSWORD", valueFrom: { secretKeyRef: { name: databaseSecret(config), key: "password" } } },
|
|
536
739
|
],
|
|
537
|
-
env: [{ name: "PGPASSWORD", valueFrom: { secretKeyRef: { name: databaseSecret(config), key: "password" } } }],
|
|
538
740
|
securityContext: restrictedContainer(),
|
|
539
741
|
resources: { requests: { cpu: "100m", memory: "128Mi" }, limits: { memory: "1Gi" } },
|
|
540
742
|
volumeMounts: [{ name: "backup", mountPath: "/backup" }],
|
|
@@ -548,12 +750,22 @@ function backup(config) {
|
|
|
548
750
|
"-c",
|
|
549
751
|
script("set -eu", 's3() { aws s3 --endpoint-url "$S3_ENDPOINT" "$@"; }',
|
|
550
752
|
// Deletes all but the newest KEEP of the backups whose names match $1, which sort by when they were made.
|
|
551
|
-
"prune() { s3 ls \"s3://$BUCKET/\" | awk '{print $4}' | grep \"$1\" | sort -r | tail -n +$((KEEP + 1)) \\", ' | while read -r old; do s3 rm "s3://$BUCKET/$old"; done; }', "stamp=$(date -u +%Y%m%dT%H%M%SZ)", 's3 cp /
|
|
753
|
+
"prune() { s3 ls \"s3://$BUCKET/\" | awk '{print $4}' | grep \"$1\" | sort -r | tail -n +$((KEEP + 1)) \\", ' | while read -r old; do s3 rm "s3://$BUCKET/$old"; done; }', "stamp=$(date -u +%Y%m%dT%H%M%SZ)", 's3 cp "$BACKUP_DIR/alasio.dump" "s3://$BUCKET/alasio-$stamp.dump"', "prune '^alasio-.*\\.dump$'", ...(workspaceStorage.enabled
|
|
552
754
|
? [
|
|
553
755
|
'meta="s3://$WORKSPACES_BUCKET/$WORKSPACES_NAME/meta/"',
|
|
554
756
|
"dump=$(s3 ls \"$meta\" | awk '{print $4}' | grep '^dump-.*\\.json\\.gz$' | sort -r | head -n 1)",
|
|
555
|
-
//
|
|
556
|
-
|
|
757
|
+
// Copied by its bytes, read and written as every object is, not by the object store's
|
|
758
|
+
// own copy (CopyObject), which SeaweedFS failed for a dump, all of the job's tries;
|
|
759
|
+
// checked as the gzip it is, and read back the same once written. None until a
|
|
760
|
+
// workspace's volume has been mounted for a while.
|
|
761
|
+
'copied="s3://$BUCKET/$WORKSPACES_NAME-$dump"',
|
|
762
|
+
'if [ -n "$dump" ]; then',
|
|
763
|
+
' s3 cp "$meta$dump" "$BACKUP_DIR/workspaces.json.gz"',
|
|
764
|
+
' gzip -t "$BACKUP_DIR/workspaces.json.gz" 2>/dev/null || { echo "$dump is no whole gzip" >&2; exit 1; }',
|
|
765
|
+
' s3 cp "$BACKUP_DIR/workspaces.json.gz" "$copied"',
|
|
766
|
+
' s3 cp "$copied" "$BACKUP_DIR/workspaces.copied"',
|
|
767
|
+
' cmp -s "$BACKUP_DIR/workspaces.json.gz" "$BACKUP_DIR/workspaces.copied" || { echo "the copy of $dump in $BUCKET is not the dump read" >&2; exit 1; }',
|
|
768
|
+
'else echo "JuiceFS has dumped no metadata of $WORKSPACES_NAME yet"; fi',
|
|
557
769
|
'prune "^$WORKSPACES_NAME-dump-.*\\.json\\.gz$"',
|
|
558
770
|
]
|
|
559
771
|
: [])),
|
|
@@ -562,13 +774,16 @@ function backup(config) {
|
|
|
562
774
|
{ name: "S3_ENDPOINT", value: s3Endpoint(config) },
|
|
563
775
|
{ name: "BUCKET", value: objectStore.buckets.backups },
|
|
564
776
|
{ name: "KEEP", value: String(config.neon.backup.keep) },
|
|
777
|
+
// Where the database's dump is, which the backup's own reads of JuiceFS's are put beside.
|
|
778
|
+
{ name: "BACKUP_DIR", value: "/backup" },
|
|
565
779
|
...(workspaceStorage.enabled
|
|
566
780
|
? [{ name: "WORKSPACES_BUCKET", value: objectStore.buckets.workspaces }, { name: "WORKSPACES_NAME", value: workspaceStorage.name }]
|
|
567
781
|
: []),
|
|
568
782
|
{ name: "AWS_DEFAULT_REGION", value: objectStore.external.region },
|
|
569
783
|
{ name: "HOME", value: "/tmp" },
|
|
570
784
|
],
|
|
571
|
-
|
|
785
|
+
// Its own identity: it writes the backups' bucket and reads what it backs up, and no more.
|
|
786
|
+
envFrom: [{ secretRef: { name: neonName("s3-backups") } }],
|
|
572
787
|
securityContext: restrictedContainer(),
|
|
573
788
|
resources: { requests: { cpu: "50m", memory: "128Mi" }, limits: { memory: "512Mi" } },
|
|
574
789
|
volumeMounts: [{ name: "backup", mountPath: "/backup" }, { name: "tmp", mountPath: "/tmp" }],
|
|
@@ -602,11 +817,11 @@ function backup(config) {
|
|
|
602
817
|
},
|
|
603
818
|
};
|
|
604
819
|
}
|
|
605
|
-
/** Neon's objects, when it runs. */
|
|
820
|
+
/** The stack's setup, which runs with Neon or without it, and Neon's objects, when it runs. */
|
|
606
821
|
export function neonObjects(config) {
|
|
607
822
|
const { neon } = config;
|
|
608
823
|
if (!neon.enabled)
|
|
609
|
-
return
|
|
824
|
+
return setup(config);
|
|
610
825
|
return [
|
|
611
826
|
...setup(config),
|
|
612
827
|
...controllerDb(config),
|
|
@@ -615,6 +830,7 @@ export function neonObjects(config) {
|
|
|
615
830
|
...safekeepers(config),
|
|
616
831
|
...pageserver(config),
|
|
617
832
|
...control(config),
|
|
833
|
+
...proxy(config),
|
|
618
834
|
...compute(config),
|
|
619
835
|
...(neon.backup.enabled ? [backup(config)] : []),
|
|
620
836
|
];
|