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.
Files changed (115) hide show
  1. package/dist/cli/src/branches.js +106 -31
  2. package/dist/cli/src/branches.js.map +1 -1
  3. package/dist/cli/src/cluster/docker-engine.js +6 -4
  4. package/dist/cli/src/cluster/docker-engine.js.map +1 -1
  5. package/dist/cli/src/cluster/docker.js +49 -10
  6. package/dist/cli/src/cluster/docker.js.map +1 -1
  7. package/dist/cli/src/cluster/host.js +45 -19
  8. package/dist/cli/src/cluster/host.js.map +1 -1
  9. package/dist/cli/src/cluster/k3s.js +104 -5
  10. package/dist/cli/src/cluster/k3s.js.map +1 -1
  11. package/dist/cli/src/cluster/node.js +102 -51
  12. package/dist/cli/src/cluster/node.js.map +1 -1
  13. package/dist/cli/src/cluster/tar.js +43 -1
  14. package/dist/cli/src/cluster/tar.js.map +1 -1
  15. package/dist/cli/src/commands/branch.js +7 -3
  16. package/dist/cli/src/commands/branch.js.map +1 -1
  17. package/dist/cli/src/commands/init.js +54 -4
  18. package/dist/cli/src/commands/init.js.map +1 -1
  19. package/dist/cli/src/commands/lake.js +40 -6
  20. package/dist/cli/src/commands/lake.js.map +1 -1
  21. package/dist/cli/src/commands/restore-filer-store.js +56 -13
  22. package/dist/cli/src/commands/restore-filer-store.js.map +1 -1
  23. package/dist/cli/src/commands/status.js +182 -10
  24. package/dist/cli/src/commands/status.js.map +1 -1
  25. package/dist/cli/src/commands/up.js +40 -3
  26. package/dist/cli/src/commands/up.js.map +1 -1
  27. package/dist/cli/src/commands/upgrade.js +22 -6
  28. package/dist/cli/src/commands/upgrade.js.map +1 -1
  29. package/dist/cli/src/config.js +20 -8
  30. package/dist/cli/src/config.js.map +1 -1
  31. package/dist/cli/src/cpu-baseline.js +97 -0
  32. package/dist/cli/src/cpu-baseline.js.map +1 -0
  33. package/dist/cli/src/install.js +94 -13
  34. package/dist/cli/src/install.js.map +1 -1
  35. package/dist/cli/src/kept-releases.js +59 -0
  36. package/dist/cli/src/kept-releases.js.map +1 -0
  37. package/dist/cli/src/kube/api.js +9 -2
  38. package/dist/cli/src/kube/api.js.map +1 -1
  39. package/dist/cli/src/kube/apply.js +1 -1
  40. package/dist/cli/src/kube/apply.js.map +1 -1
  41. package/dist/cli/src/kube/rollout.js +79 -12
  42. package/dist/cli/src/kube/rollout.js.map +1 -1
  43. package/dist/cli/src/lake.js +124 -0
  44. package/dist/cli/src/lake.js.map +1 -0
  45. package/dist/cli/src/manifests/admission.js +183 -24
  46. package/dist/cli/src/manifests/admission.js.map +1 -1
  47. package/dist/cli/src/manifests/alasio.js +198 -27
  48. package/dist/cli/src/manifests/alasio.js.map +1 -1
  49. package/dist/cli/src/manifests/branch.js +2 -1
  50. package/dist/cli/src/manifests/branch.js.map +1 -1
  51. package/dist/cli/src/manifests/collector.js +229 -122
  52. package/dist/cli/src/manifests/collector.js.map +1 -1
  53. package/dist/cli/src/manifests/common.js +23 -8
  54. package/dist/cli/src/manifests/common.js.map +1 -1
  55. package/dist/cli/src/manifests/config.js +254 -21
  56. package/dist/cli/src/manifests/config.js.map +1 -1
  57. package/dist/cli/src/manifests/grafana.js +18 -6
  58. package/dist/cli/src/manifests/grafana.js.map +1 -1
  59. package/dist/cli/src/manifests/index.js +15 -6
  60. package/dist/cli/src/manifests/index.js.map +1 -1
  61. package/dist/cli/src/manifests/juicefs-csi.js +7 -6
  62. package/dist/cli/src/manifests/juicefs-csi.js.map +1 -1
  63. package/dist/cli/src/manifests/keda.js +2 -1
  64. package/dist/cli/src/manifests/keda.js.map +1 -1
  65. package/dist/cli/src/manifests/lake.js +340 -73
  66. package/dist/cli/src/manifests/lake.js.map +1 -1
  67. package/dist/cli/src/manifests/log.js +298 -0
  68. package/dist/cli/src/manifests/log.js.map +1 -0
  69. package/dist/cli/src/manifests/neon.js +282 -66
  70. package/dist/cli/src/manifests/neon.js.map +1 -1
  71. package/dist/cli/src/manifests/network-policies.js +211 -45
  72. package/dist/cli/src/manifests/network-policies.js.map +1 -1
  73. package/dist/cli/src/manifests/node-agent.js +116 -0
  74. package/dist/cli/src/manifests/node-agent.js.map +1 -0
  75. package/dist/cli/src/manifests/object-store.js +42 -10
  76. package/dist/cli/src/manifests/object-store.js.map +1 -1
  77. package/dist/cli/src/manifests/priorities.js +1 -0
  78. package/dist/cli/src/manifests/priorities.js.map +1 -1
  79. package/dist/cli/src/manifests/scalers.js.map +1 -1
  80. package/dist/cli/src/manifests/valkey.js +1 -1
  81. package/dist/cli/src/manifests/webhook.js +102 -0
  82. package/dist/cli/src/manifests/webhook.js.map +1 -0
  83. package/dist/cli/src/neon-storage.js +115 -0
  84. package/dist/cli/src/neon-storage.js.map +1 -0
  85. package/dist/cli/src/release.js +9 -6
  86. package/dist/cli/src/release.js.map +1 -1
  87. package/dist/cli/src/secrets.js +45 -4
  88. package/dist/cli/src/secrets.js.map +1 -1
  89. package/dist/cli/src/target.js +1 -1
  90. package/dist/cli/src/target.js.map +1 -1
  91. package/dist/cli/src/telegram.js +56 -10
  92. package/dist/cli/src/telegram.js.map +1 -1
  93. package/dist/cli/src/webhook.js +182 -0
  94. package/dist/cli/src/webhook.js.map +1 -0
  95. package/dist/cluster/node/pins.json +11 -2
  96. package/dist/neon/control/branches.js +146 -22
  97. package/dist/neon/control/branches.js.map +1 -1
  98. package/dist/neon/control/endpoints.js +117 -0
  99. package/dist/neon/control/endpoints.js.map +1 -0
  100. package/dist/neon/control/jwt.js +16 -3
  101. package/dist/neon/control/jwt.js.map +1 -1
  102. package/dist/neon/control/scram.js +13 -1
  103. package/dist/neon/control/scram.js.map +1 -1
  104. package/dist/neon/control/secrets.js +88 -14
  105. package/dist/neon/control/secrets.js.map +1 -1
  106. package/dist/src/kube/config.js +27 -3
  107. package/dist/src/kube/config.js.map +1 -1
  108. package/dist/src/kube/sandbox-shape.js +49 -3
  109. package/dist/src/kube/sandbox-shape.js.map +1 -1
  110. package/dist/src/node-agent/names.js +140 -0
  111. package/dist/src/node-agent/names.js.map +1 -0
  112. package/dist/src/telegram/deliveries.js +40 -0
  113. package/dist/src/telegram/deliveries.js.map +1 -0
  114. package/package.json +2 -2
  115. 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 { claimSpec, collectorRuns, databaseSecret, given, helperResources, imagePullSecrets, MAIN_ENVIRONMENT, NAMESPACE, neonName, ONE, otelEnv, RELEASE, replicasOf, restrictedContainer, s3Endpoint, script, selectorLabels, stackLabels, stackPodSpec, waitFor, waitForObjectStore, } from "./common.js";
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, workspace
24
- * storage's among them when it is on, by neon/control/kube-setup.ts: a Job, which applying alasio runs to completion before
25
- * anything else of alasio starts (../kube/apply.ts).
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
- { name: "NEON_BROKER_URL", value: `http://${neonName("storage-broker")}:50051` },
68
- { name: "NEON_CONTROLLER_URL", value: `http://${neonName("storage-controller")}:1234` },
69
- { name: "NEON_PAGESERVER_HOST", value: neonName("pageserver") },
70
- { name: "NEON_COMPUTE_HOST", value: neonName("compute") },
71
- { name: "NEON_CONTROLLER_DB_HOST", value: neonName("controller-db") },
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("floor"),
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("floor"),
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 pageserver: pages served from layers it keeps in the object store, its volume a
320
- * cache. It registers itself with the controller by the name in its metadata.
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: name,
332
- ...replicasOf(ONE),
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("floor"),
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, rendered by the stack's setup, into its working directory.
346
- command: ["/bin/sh", "-c", "cp /config/pageserver.toml /config/identity.toml /config/metadata.json /config/auth_public_key.pem /data/.neon/"],
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: config.neon.pageserver.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(config.neon.pageserver.storage) }],
436
+ volumeClaimTemplates: [{ metadata: { name: "data" }, spec: claimSpec(storage) }],
376
437
  },
377
438
  };
378
- return [service(name, component, [{ name: "pg", port: 6400 }, { name: "http", port: 9898 }]), statefulSet, verticalPodAutoscaler(statefulSet, "Recommended")];
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, and
383
- * repairs a safekeeper that lost its disk. Its record of the branches is on a volume of
384
- * its own.
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 claim = {
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(ONE),
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
- // As every compute reaches it, a branch environment's in its own namespace too.
419
- { name: "NEON_PAGESERVER_HOST", value: `${neonName("pageserver")}.${NAMESPACE}.svc` },
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: config.neon.control.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 [service(name, component, [{ name: "http", port: 8080 }]), claim, deployment, verticalPodAutoscaler(deployment, "Recommended")];
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
- const name = neonName("compute");
459
- const component = "neon-compute";
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: { name, namespace: environment.namespace, labels: stackLabels(component) },
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 sleeps.
473
- ...priority(environment.branch === null ? "floor" : "component"),
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
- "postgresql://cloud_admin@localhost:55433/postgres",
678
+ `postgresql://cloud_admin@localhost:${COMPUTE_PORT}/postgres`,
490
679
  "--pgbin",
491
680
  "/usr/local/bin/postgres",
492
681
  "--compute-id",
493
- computeId(environment.branch ?? MAIN),
682
+ replica ? replicaId(branch) : computeId(branch),
494
683
  "--control-plane-uri",
495
684
  control,
496
685
  ],
497
686
  env: [
498
- secretVariable(name, "NEON_CONTROL_PLANE_TOKEN"),
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: 55433 }, { name: "http", containerPort: 3080 }],
504
- readinessProbe: { exec: { command: ["pg_isready", "-h", "127.0.0.1", "-p", "55433", "-U", "cloud_admin", "-d", "postgres"] }, periodSeconds: 5 },
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: config.neon.compute.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
- return [service(name, component, [{ name: "postgres", port: 55433 }, { name: "http", port: 3080 }], environment), deployment, verticalPodAutoscaler(deployment, "Recommended")];
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 compute is
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
- 'until pg_isready --quiet --host="$0" --port=55433; do sleep 2; done && pg_dump --host="$0" --port=55433 --username=alasio --dbname=alasio --format=custom --file=/backup/alasio.dump',
535
- neonName("compute"),
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 /backup/alasio.dump "s3://$BUCKET/alasio-$stamp.dump"', "prune '^alasio-.*\\.dump$'", ...(workspaceStorage.enabled
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
- // None until a workspace's volume has been mounted for a while.
556
- 'if [ -n "$dump" ]; then s3 cp "$meta$dump" "s3://$BUCKET/$WORKSPACES_NAME-$dump"; else echo "JuiceFS has dumped no metadata of $WORKSPACES_NAME yet"; fi',
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
- envFrom: [{ secretRef: { name: neonName("s3-admin") } }],
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
  ];