alasio 4.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 (72) hide show
  1. package/dist/cli/src/cluster/docker.js +219 -0
  2. package/dist/cli/src/cluster/docker.js.map +1 -0
  3. package/dist/cli/src/cluster/local.js +392 -0
  4. package/dist/cli/src/cluster/local.js.map +1 -0
  5. package/dist/cli/src/commands/common.js +108 -0
  6. package/dist/cli/src/commands/common.js.map +1 -0
  7. package/dist/cli/src/commands/down.js +26 -0
  8. package/dist/cli/src/commands/down.js.map +1 -0
  9. package/dist/cli/src/commands/init.js +314 -0
  10. package/dist/cli/src/commands/init.js.map +1 -0
  11. package/dist/cli/src/commands/lake.js +33 -0
  12. package/dist/cli/src/commands/lake.js.map +1 -0
  13. package/dist/cli/src/commands/login.js +38 -0
  14. package/dist/cli/src/commands/login.js.map +1 -0
  15. package/dist/cli/src/commands/logs.js +27 -0
  16. package/dist/cli/src/commands/logs.js.map +1 -0
  17. package/dist/cli/src/commands/restart.js +23 -0
  18. package/dist/cli/src/commands/restart.js.map +1 -0
  19. package/dist/cli/src/commands/status.js +58 -0
  20. package/dist/cli/src/commands/status.js.map +1 -0
  21. package/dist/cli/src/commands/uninstall.js +47 -0
  22. package/dist/cli/src/commands/uninstall.js.map +1 -0
  23. package/dist/cli/src/commands/up.js +33 -0
  24. package/dist/cli/src/commands/up.js.map +1 -0
  25. package/dist/cli/src/commands/upgrade.js +50 -0
  26. package/dist/cli/src/commands/upgrade.js.map +1 -0
  27. package/dist/cli/src/commands.js +16 -0
  28. package/dist/cli/src/commands.js.map +1 -0
  29. package/dist/cli/src/config.js +169 -0
  30. package/dist/cli/src/config.js.map +1 -0
  31. package/dist/cli/src/images.js +7 -0
  32. package/dist/cli/src/images.js.map +1 -0
  33. package/dist/cli/src/install.js +39 -0
  34. package/dist/cli/src/install.js.map +1 -0
  35. package/dist/cli/src/kube/api.js +231 -0
  36. package/dist/cli/src/kube/api.js.map +1 -0
  37. package/dist/cli/src/kube/apply.js +98 -0
  38. package/dist/cli/src/kube/apply.js.map +1 -0
  39. package/dist/cli/src/kube/rollout.js +166 -0
  40. package/dist/cli/src/kube/rollout.js.map +1 -0
  41. package/dist/cli/src/main.js +30 -0
  42. package/dist/cli/src/main.js.map +1 -0
  43. package/dist/cli/src/manifests/agent-sandbox.js +89 -0
  44. package/dist/cli/src/manifests/agent-sandbox.js.map +1 -0
  45. package/dist/cli/src/manifests/alasio.js +367 -0
  46. package/dist/cli/src/manifests/alasio.js.map +1 -0
  47. package/dist/cli/src/manifests/common.js +157 -0
  48. package/dist/cli/src/manifests/common.js.map +1 -0
  49. package/dist/cli/src/manifests/config.js +283 -0
  50. package/dist/cli/src/manifests/config.js.map +1 -0
  51. package/dist/cli/src/manifests/index.js +20 -0
  52. package/dist/cli/src/manifests/index.js.map +1 -0
  53. package/dist/cli/src/manifests/lake.js +56 -0
  54. package/dist/cli/src/manifests/lake.js.map +1 -0
  55. package/dist/cli/src/manifests/neon.js +656 -0
  56. package/dist/cli/src/manifests/neon.js.map +1 -0
  57. package/dist/cli/src/manifests/network-policies.js +86 -0
  58. package/dist/cli/src/manifests/network-policies.js.map +1 -0
  59. package/dist/cli/src/manifests/object-store.js +204 -0
  60. package/dist/cli/src/manifests/object-store.js.map +1 -0
  61. package/dist/cli/src/manifests/sandboxes.agents.x-k8s.io.json +6295 -0
  62. package/dist/cli/src/release.js +15 -0
  63. package/dist/cli/src/release.js.map +1 -0
  64. package/dist/cli/src/secrets.js +49 -0
  65. package/dist/cli/src/secrets.js.map +1 -0
  66. package/dist/cli/src/target.js +55 -0
  67. package/dist/cli/src/target.js.map +1 -0
  68. package/dist/cli/src/telegram.js +27 -0
  69. package/dist/cli/src/telegram.js.map +1 -0
  70. package/dist/src/kube/config.js +99 -0
  71. package/dist/src/kube/config.js.map +1 -0
  72. package/package.json +32 -0
@@ -0,0 +1,656 @@
1
+ import { imageReference } from "../images.js";
2
+ import { claimSpec, componentName, databaseSecret, given, goJson, helperResources, imagePullSecrets, NAMESPACE, neonName, otelEnv, RELEASE, restrictedContainer, s3Endpoint, script, selectorLabels, sha256, stackLabels, stackPodSpec, waitFor, waitForObjectStore, } from "./common.js";
3
+ /** A Service of the stack's: its name and component, selecting the component's pods, on `ports`. */
4
+ function service(name, component, ports) {
5
+ return {
6
+ apiVersion: "v1",
7
+ kind: "Service",
8
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
9
+ spec: { selector: selectorLabels(component), ports },
10
+ };
11
+ }
12
+ /** A Secret's key, as a variable of the same name. */
13
+ function secretVariable(secret, key) {
14
+ return { name: key, valueFrom: { secretKeyRef: { name: secret, key } } };
15
+ }
16
+ /**
17
+ * The stack's secrets, made once and kept, and each service's rendered from them, by
18
+ * neon/control/kube-setup.ts: a Job, which applying alasio runs to completion before
19
+ * anything else of alasio starts (../kube/apply.ts).
20
+ */
21
+ function setup(config) {
22
+ const name = neonName("setup");
23
+ const { objectStore } = config;
24
+ const metadata = { name, namespace: NAMESPACE, labels: stackLabels("neon-setup") };
25
+ const serviceAccount = { apiVersion: "v1", kind: "ServiceAccount", metadata };
26
+ const role = {
27
+ apiVersion: "rbac.authorization.k8s.io/v1",
28
+ kind: "Role",
29
+ metadata,
30
+ rules: [{ apiGroups: [""], resources: ["secrets"], verbs: ["get", "create", "update"] }],
31
+ };
32
+ const binding = {
33
+ apiVersion: "rbac.authorization.k8s.io/v1",
34
+ kind: "RoleBinding",
35
+ metadata,
36
+ subjects: [{ kind: "ServiceAccount", name, namespace: NAMESPACE }],
37
+ roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name },
38
+ };
39
+ const job = {
40
+ apiVersion: "batch/v1",
41
+ kind: "Job",
42
+ metadata,
43
+ spec: {
44
+ backoffLimit: 3,
45
+ activeDeadlineSeconds: 300,
46
+ template: {
47
+ metadata: { labels: stackLabels("neon-setup") },
48
+ spec: {
49
+ serviceAccountName: name,
50
+ restartPolicy: "Never",
51
+ ...stackPodSpec(config),
52
+ containers: [{
53
+ name: "setup",
54
+ image: imageReference(config.images.alasio),
55
+ imagePullPolicy: config.images.pullPolicy,
56
+ command: ["node", "/opt/alasio/neon/control/kube-setup.ts"],
57
+ env: [
58
+ { name: "NAMESPACE", valueFrom: { fieldRef: { fieldPath: "metadata.namespace" } } },
59
+ { name: "SECRET_PREFIX", value: RELEASE },
60
+ { name: "NEON_BROKER_URL", value: `http://${neonName("storage-broker")}:50051` },
61
+ { name: "NEON_CONTROLLER_URL", value: `http://${neonName("storage-controller")}:1234` },
62
+ { name: "NEON_PAGESERVER_HOST", value: neonName("pageserver") },
63
+ { name: "NEON_COMPUTE_HOST", value: neonName("compute") },
64
+ { name: "NEON_CONTROLLER_DB_HOST", value: neonName("controller-db") },
65
+ { name: "S3_ENDPOINT", value: s3Endpoint(config) },
66
+ { name: "S3_REGION", value: objectStore.external.region },
67
+ { name: "S3_BUCKET_NEON", value: objectStore.buckets.neon },
68
+ { name: "S3_BUCKET_LAKE", value: objectStore.buckets.lake },
69
+ ...(objectStore.bundled.enabled ? [] : [
70
+ { name: "S3_EXTERNAL", value: "1" },
71
+ { name: "S3_ACCESS_KEY", valueFrom: { secretKeyRef: { name: objectStore.external.existingSecret, key: "accessKey" } } },
72
+ { name: "S3_SECRET_KEY", valueFrom: { secretKeyRef: { name: objectStore.external.existingSecret, key: "secretKey" } } },
73
+ ]),
74
+ ],
75
+ securityContext: restrictedContainer(),
76
+ resources: { requests: { cpu: "50m", memory: "64Mi" }, limits: { memory: "256Mi" } },
77
+ }],
78
+ },
79
+ },
80
+ },
81
+ };
82
+ return [serviceAccount, role, binding, job];
83
+ }
84
+ /** The storage controller's own Postgres. */
85
+ function controllerDb(config) {
86
+ const name = neonName("controller-db");
87
+ const component = "neon-controller-db";
88
+ const statefulSet = {
89
+ apiVersion: "apps/v1",
90
+ kind: "StatefulSet",
91
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
92
+ spec: {
93
+ serviceName: name,
94
+ replicas: 1,
95
+ selector: { matchLabels: selectorLabels(component) },
96
+ template: {
97
+ metadata: { labels: stackLabels(component) },
98
+ spec: {
99
+ ...stackPodSpec(config),
100
+ containers: [{
101
+ name: "postgres",
102
+ image: imageReference(config.neon.controllerDbImage),
103
+ imagePullPolicy: "IfNotPresent",
104
+ env: [
105
+ { name: "POSTGRES_USER", value: "storage_controller" },
106
+ { name: "POSTGRES_DB", value: "storage_controller" },
107
+ { name: "PGDATA", value: "/var/lib/postgresql/data/pgdata" },
108
+ ],
109
+ envFrom: [{ secretRef: { name } }],
110
+ ports: [{ name: "postgres", containerPort: 5432 }],
111
+ readinessProbe: { exec: { command: ["pg_isready", "-U", "storage_controller", "-d", "storage_controller"] }, periodSeconds: 5 },
112
+ securityContext: restrictedContainer(),
113
+ resources: config.neon.controllerDb.resources,
114
+ volumeMounts: [{ name: "data", mountPath: "/var/lib/postgresql/data" }, { name: "run", mountPath: "/var/run/postgresql" }],
115
+ }],
116
+ volumes: [{ name: "run", emptyDir: {} }],
117
+ },
118
+ },
119
+ volumeClaimTemplates: [{ metadata: { name: "data" }, spec: claimSpec(config.neon.controllerDb.storage) }],
120
+ },
121
+ };
122
+ return [service(name, component, [{ name: "postgres", port: 5432 }]), statefulSet];
123
+ }
124
+ /** The storage broker, through which safekeepers and the pageserver learn of each other's progress. */
125
+ function storageBroker(config) {
126
+ const name = neonName("storage-broker");
127
+ const component = "neon-storage-broker";
128
+ const deployment = {
129
+ apiVersion: "apps/v1",
130
+ kind: "Deployment",
131
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
132
+ spec: {
133
+ replicas: 1,
134
+ selector: { matchLabels: selectorLabels(component) },
135
+ template: {
136
+ metadata: { labels: stackLabels(component) },
137
+ spec: {
138
+ ...stackPodSpec(config),
139
+ containers: [{
140
+ name: "storage-broker",
141
+ image: imageReference(config.neon.image),
142
+ imagePullPolicy: "IfNotPresent",
143
+ command: ["storage_broker", "--listen-addr=0.0.0.0:50051"],
144
+ ports: [{ name: "grpc", containerPort: 50051 }],
145
+ readinessProbe: { tcpSocket: { port: "grpc" }, periodSeconds: 5 },
146
+ securityContext: restrictedContainer(),
147
+ resources: config.neon.storageBroker.resources,
148
+ }],
149
+ },
150
+ },
151
+ },
152
+ };
153
+ return [service(name, component, [{ name: "grpc", port: 50051 }]), deployment];
154
+ }
155
+ /** The storage controller: one at a time, as it leads the storage it controls; strict, every timeline on every safekeeper. */
156
+ function storageController(config) {
157
+ const name = neonName("storage-controller");
158
+ const component = "neon-storage-controller";
159
+ const deployment = {
160
+ apiVersion: "apps/v1",
161
+ kind: "Deployment",
162
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
163
+ spec: {
164
+ replicas: 1,
165
+ strategy: { type: "Recreate" },
166
+ selector: { matchLabels: selectorLabels(component) },
167
+ template: {
168
+ metadata: { labels: stackLabels(component) },
169
+ spec: {
170
+ ...stackPodSpec(config),
171
+ initContainers: [{
172
+ name: "wait-for-controller-db",
173
+ image: imageReference(config.neon.controllerDbImage),
174
+ imagePullPolicy: "IfNotPresent",
175
+ command: ["/bin/sh", "-c", 'until pg_isready -h "$0" -U storage_controller; do sleep 2; done', neonName("controller-db")],
176
+ securityContext: restrictedContainer(),
177
+ ...helperResources(),
178
+ }],
179
+ containers: [{
180
+ name: "storage-controller",
181
+ image: imageReference(config.neon.image),
182
+ imagePullPolicy: "IfNotPresent",
183
+ command: [
184
+ "/bin/sh",
185
+ "-c",
186
+ [
187
+ 'PUBLIC_KEY="$(cat /keys/auth_public_key.pem)"',
188
+ "exec storage_controller",
189
+ "--listen 0.0.0.0:1234",
190
+ `--control-plane-url http://${neonName("control")}:8080`,
191
+ "--timelines-onto-safekeepers",
192
+ `--timeline-safekeeper-count ${config.neon.safekeepers.replicas}`,
193
+ ].join(" "),
194
+ ],
195
+ envFrom: [{ secretRef: { name } }],
196
+ ports: [{ name: "http", containerPort: 1234 }],
197
+ readinessProbe: { httpGet: { path: "/status", port: "http" }, periodSeconds: 5 },
198
+ securityContext: restrictedContainer(),
199
+ resources: config.neon.storageController.resources,
200
+ volumeMounts: [{ name: "keys", mountPath: "/keys", readOnly: true }],
201
+ }],
202
+ volumes: [{ name: "keys", secret: { secretName: name, items: [{ key: "auth_public_key.pem", path: "auth_public_key.pem" }] } }],
203
+ },
204
+ },
205
+ },
206
+ };
207
+ return [service(name, component, [{ name: "http", port: 1234 }]), deployment];
208
+ }
209
+ /**
210
+ * The safekeepers: a timeline's WAL, each on its own volume, at stable names neon-control
211
+ * registers them by, which resolve before they are ready, as the controller needs. They
212
+ * spread across nodes and zones where the cluster has them, so a quorum survives one.
213
+ */
214
+ function safekeepers(config) {
215
+ const name = neonName("safekeeper");
216
+ const component = "neon-safekeeper";
217
+ const spread = (topologyKey) => ({
218
+ maxSkew: 1,
219
+ topologyKey,
220
+ whenUnsatisfiable: "ScheduleAnyway",
221
+ labelSelector: { matchLabels: selectorLabels(component) },
222
+ });
223
+ const headless = {
224
+ apiVersion: "v1",
225
+ kind: "Service",
226
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
227
+ spec: { clusterIP: "None", publishNotReadyAddresses: true, selector: selectorLabels(component), ports: [{ name: "pg", port: 5454 }, { name: "http", port: 7676 }] },
228
+ };
229
+ const statefulSet = {
230
+ apiVersion: "apps/v1",
231
+ kind: "StatefulSet",
232
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
233
+ spec: {
234
+ serviceName: name,
235
+ replicas: config.neon.safekeepers.replicas,
236
+ podManagementPolicy: "Parallel",
237
+ selector: { matchLabels: selectorLabels(component) },
238
+ template: {
239
+ metadata: { labels: stackLabels(component) },
240
+ spec: {
241
+ ...stackPodSpec(config),
242
+ terminationGracePeriodSeconds: 30,
243
+ ...given("initContainers", waitForObjectStore(config)),
244
+ topologySpreadConstraints: [spread("kubernetes.io/hostname"), spread("topology.kubernetes.io/zone")],
245
+ containers: [{
246
+ name: "safekeeper",
247
+ image: imageReference(config.neon.image),
248
+ imagePullPolicy: "IfNotPresent",
249
+ // Its id is its ordinal plus one; each is its own availability zone, so each
250
+ // timeline is placed on distinct safekeepers.
251
+ command: [
252
+ "/bin/sh",
253
+ "-c",
254
+ [
255
+ "SAFEKEEPER_ID=$((POD_INDEX + 1)) &&",
256
+ 'exec safekeeper -D /data --id "$SAFEKEEPER_ID"',
257
+ `--listen-pg 0.0.0.0:5454 --advertise-pg "$POD_NAME.${name}.$POD_NAMESPACE.svc.cluster.local:5454"`,
258
+ "--listen-http 0.0.0.0:7676",
259
+ '--availability-zone "az-safekeeper-$SAFEKEEPER_ID"',
260
+ `--broker-endpoint http://${neonName("storage-broker")}:50051`,
261
+ '--remote-storage "$REMOTE_STORAGE"',
262
+ "--pg-auth-public-key-path /keys/auth_public_key.pem",
263
+ "--pg-tenant-only-auth-public-key-path /keys/auth_public_key.pem",
264
+ "--http-auth-public-key-path /keys/auth_public_key.pem",
265
+ "--auth-token-path /keys/safekeeper_peer_token",
266
+ ].join(" "),
267
+ ],
268
+ env: [
269
+ { name: "POD_NAME", valueFrom: { fieldRef: { fieldPath: "metadata.name" } } },
270
+ { name: "POD_NAMESPACE", valueFrom: { fieldRef: { fieldPath: "metadata.namespace" } } },
271
+ { name: "POD_INDEX", valueFrom: { fieldRef: { fieldPath: "metadata.labels['apps.kubernetes.io/pod-index']" } } },
272
+ secretVariable(name, "AWS_ACCESS_KEY_ID"),
273
+ secretVariable(name, "AWS_SECRET_ACCESS_KEY"),
274
+ secretVariable(name, "REMOTE_STORAGE"),
275
+ ],
276
+ ports: [{ name: "pg", containerPort: 5454 }, { name: "http", containerPort: 7676 }],
277
+ readinessProbe: { httpGet: { path: "/v1/status", port: "http" }, periodSeconds: 5 },
278
+ securityContext: restrictedContainer(),
279
+ resources: config.neon.safekeepers.resources,
280
+ volumeMounts: [{ name: "data", mountPath: "/data" }, { name: "keys", mountPath: "/keys", readOnly: true }],
281
+ }],
282
+ volumes: [{
283
+ name: "keys",
284
+ secret: {
285
+ secretName: name,
286
+ defaultMode: 0o440,
287
+ items: [{ key: "auth_public_key.pem", path: "auth_public_key.pem" }, { key: "safekeeper_peer_token", path: "safekeeper_peer_token" }],
288
+ },
289
+ }],
290
+ },
291
+ },
292
+ volumeClaimTemplates: [{ metadata: { name: "data" }, spec: claimSpec(config.neon.safekeepers.storage) }],
293
+ },
294
+ };
295
+ return [headless, statefulSet];
296
+ }
297
+ /**
298
+ * The pageserver: pages served from layers it keeps in the object store, its volume a
299
+ * cache. It registers itself with the controller by the name in its metadata.
300
+ */
301
+ function pageserver(config) {
302
+ const name = neonName("pageserver");
303
+ const component = "neon-pageserver";
304
+ const image = imageReference(config.neon.image);
305
+ const statefulSet = {
306
+ apiVersion: "apps/v1",
307
+ kind: "StatefulSet",
308
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
309
+ spec: {
310
+ serviceName: name,
311
+ replicas: 1,
312
+ selector: { matchLabels: selectorLabels(component) },
313
+ template: {
314
+ metadata: { labels: stackLabels(component) },
315
+ spec: {
316
+ ...stackPodSpec(config),
317
+ terminationGracePeriodSeconds: 30,
318
+ initContainers: [
319
+ {
320
+ name: "config",
321
+ image,
322
+ imagePullPolicy: "IfNotPresent",
323
+ // Its configuration, rendered by the stack's setup, into its working directory.
324
+ command: ["/bin/sh", "-c", "cp /config/pageserver.toml /config/identity.toml /config/metadata.json /config/auth_public_key.pem /data/.neon/"],
325
+ securityContext: restrictedContainer(),
326
+ ...helperResources(),
327
+ volumeMounts: [{ name: "data", mountPath: "/data/.neon" }, { name: "config", mountPath: "/config", readOnly: true }],
328
+ },
329
+ waitFor(config, "storage-controller", `http://${neonName("storage-controller")}:1234/status`),
330
+ ...waitForObjectStore(config),
331
+ ],
332
+ containers: [{
333
+ name: "pageserver",
334
+ image,
335
+ imagePullPolicy: "IfNotPresent",
336
+ command: ["pageserver", "-D", "/data/.neon"],
337
+ env: [secretVariable(name, "AWS_ACCESS_KEY_ID"), secretVariable(name, "AWS_SECRET_ACCESS_KEY"), secretVariable(name, "NEON_AUTH_TOKEN")],
338
+ ports: [{ name: "pg", containerPort: 6400 }, { name: "http", containerPort: 9898 }],
339
+ readinessProbe: { httpGet: { path: "/v1/status", port: "http" }, periodSeconds: 5 },
340
+ securityContext: restrictedContainer(),
341
+ resources: config.neon.pageserver.resources,
342
+ volumeMounts: [{ name: "data", mountPath: "/data/.neon" }],
343
+ }],
344
+ volumes: [{
345
+ name: "config",
346
+ secret: {
347
+ secretName: name,
348
+ items: ["pageserver.toml", "identity.toml", "metadata.json", "auth_public_key.pem"].map((key) => ({ key, path: key })),
349
+ },
350
+ }],
351
+ },
352
+ },
353
+ volumeClaimTemplates: [{ metadata: { name: "data" }, spec: claimSpec(config.neon.pageserver.storage) }],
354
+ },
355
+ };
356
+ return [service(name, component, [{ name: "pg", port: 6400 }, { name: "http", port: 9898 }]), statefulSet];
357
+ }
358
+ /**
359
+ * neon-control: it bootstraps the tenant and timeline, serves the compute its spec,
360
+ * answers the storage controller's hooks, and repairs a safekeeper that lost its disk.
361
+ * Its record of what it bootstrapped is on a volume of its own.
362
+ */
363
+ function control(config) {
364
+ const name = neonName("control");
365
+ const component = "neon-control";
366
+ const safekeeper = neonName("safekeeper");
367
+ const safekeeperHosts = Array.from({ length: config.neon.safekeepers.replicas }, (_, index) => `${safekeeper}-${index}.${safekeeper}.${NAMESPACE}.svc.cluster.local`);
368
+ const claim = {
369
+ apiVersion: "v1",
370
+ kind: "PersistentVolumeClaim",
371
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
372
+ spec: claimSpec(config.neon.control.storage),
373
+ };
374
+ const deployment = {
375
+ apiVersion: "apps/v1",
376
+ kind: "Deployment",
377
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
378
+ spec: {
379
+ replicas: 1,
380
+ strategy: { type: "Recreate" },
381
+ selector: { matchLabels: selectorLabels(component) },
382
+ template: {
383
+ metadata: { labels: stackLabels(component) },
384
+ spec: {
385
+ ...stackPodSpec(config),
386
+ containers: [{
387
+ name: "neon-control",
388
+ image: imageReference(config.images.alasio),
389
+ imagePullPolicy: config.images.pullPolicy,
390
+ command: ["node", "/opt/alasio/neon/control/service.ts"],
391
+ env: [
392
+ { name: "CONTROLLER_URL", value: `http://${neonName("storage-controller")}:1234` },
393
+ { name: "NEON_SAFEKEEPER_HOSTS", value: safekeeperHosts.join(",") },
394
+ { name: "NEON_PAGESERVER_HOST", value: neonName("pageserver") },
395
+ ],
396
+ ports: [{ name: "http", containerPort: 8080 }],
397
+ readinessProbe: { httpGet: { path: "/healthz", port: "http" }, periodSeconds: 3 },
398
+ securityContext: { ...restrictedContainer(), readOnlyRootFilesystem: true },
399
+ resources: config.neon.control.resources,
400
+ volumeMounts: [
401
+ { name: "state", mountPath: "/state" },
402
+ { name: "root", mountPath: "/secrets", readOnly: true },
403
+ { name: "keys", mountPath: "/keys", readOnly: true },
404
+ ],
405
+ }],
406
+ volumes: [
407
+ { name: "state", persistentVolumeClaim: { claimName: name } },
408
+ {
409
+ name: "root",
410
+ secret: {
411
+ secretName: neonName("root"),
412
+ defaultMode: 0o440,
413
+ items: [{ key: "secrets.json", path: "secrets.json" }, { key: "auth_private_key.pem", path: "auth_private_key.pem" }],
414
+ },
415
+ },
416
+ { name: "keys", secret: { secretName: neonName("root"), items: [{ key: "auth_public_key.pem", path: "auth_public_key.pem" }] } },
417
+ ],
418
+ },
419
+ },
420
+ },
421
+ };
422
+ return [service(name, component, [{ name: "http", port: 8080 }]), claim, deployment];
423
+ }
424
+ /**
425
+ * The compute: Postgres on alasio's timeline, one primary (never two on one timeline),
426
+ * its own disk rebuilt from the safekeepers and pageserver on every start, its spec
427
+ * fetched from neon-control. It serves compute_ctl's metrics too, which the collector
428
+ * scrapes, and traces where alasio's telemetry goes, or nowhere.
429
+ */
430
+ function compute(config) {
431
+ const name = neonName("compute");
432
+ const component = "neon-compute";
433
+ const control = neonName("control");
434
+ const deployment = {
435
+ apiVersion: "apps/v1",
436
+ kind: "Deployment",
437
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
438
+ spec: {
439
+ replicas: 1,
440
+ strategy: { type: "Recreate" },
441
+ selector: { matchLabels: selectorLabels(component) },
442
+ template: {
443
+ metadata: { labels: stackLabels(component) },
444
+ spec: {
445
+ ...imagePullSecrets(config),
446
+ // The compute image runs as its own postgres user.
447
+ securityContext: { fsGroup: config.neon.runAsGroup, seccompProfile: { type: "RuntimeDefault" } },
448
+ ...given("nodeSelector", { ...config.neon.nodeSelector }),
449
+ ...given("tolerations", [...config.neon.tolerations]),
450
+ terminationGracePeriodSeconds: 30,
451
+ initContainers: [waitFor(config, "neon-control", `http://${control}:8080/healthz`)],
452
+ containers: [{
453
+ name: "compute",
454
+ image: imageReference(config.neon.computeImage),
455
+ imagePullPolicy: "IfNotPresent",
456
+ args: [
457
+ "--pgdata",
458
+ "/var/db/postgres/compute",
459
+ "--connstr",
460
+ "postgresql://cloud_admin@localhost:55433/postgres",
461
+ "--pgbin",
462
+ "/usr/local/bin/postgres",
463
+ "--compute-id",
464
+ "alasio",
465
+ "--control-plane-uri",
466
+ `http://${control}:8080`,
467
+ ],
468
+ env: [
469
+ secretVariable(name, "NEON_CONTROL_PLANE_TOKEN"),
470
+ ...(config.telemetry.otlpEndpoint
471
+ ? [{ name: "OTEL_SERVICE_NAME", value: "compute_ctl" }, ...otelEnv(config)]
472
+ : [{ name: "OTEL_SDK_DISABLED", value: "true" }]),
473
+ ],
474
+ ports: [{ name: "postgres", containerPort: 55433 }, { name: "http", containerPort: 3080 }],
475
+ readinessProbe: { exec: { command: ["pg_isready", "-h", "127.0.0.1", "-p", "55433", "-U", "cloud_admin", "-d", "postgres"] }, periodSeconds: 5 },
476
+ securityContext: restrictedContainer(),
477
+ resources: config.neon.compute.resources,
478
+ volumeMounts: [{ name: "pgdata", mountPath: "/var/db/postgres" }],
479
+ }],
480
+ volumes: [{ name: "pgdata", emptyDir: {} }],
481
+ },
482
+ },
483
+ },
484
+ };
485
+ return [service(name, component, [{ name: "postgres", port: 55433 }, { name: "http", port: 3080 }]), deployment];
486
+ }
487
+ /**
488
+ * A logical dump of alasio's database, restorable into any Postgres whatever becomes of
489
+ * the stack, to the object store's backups bucket, of which the last `keep` are kept.
490
+ */
491
+ function backup(config) {
492
+ const component = "neon-backup";
493
+ const dump = {
494
+ name: "dump",
495
+ image: imageReference(config.neon.computeImage),
496
+ imagePullPolicy: "IfNotPresent",
497
+ command: [
498
+ "/bin/sh",
499
+ "-c",
500
+ // A new pod's NetworkPolicy may apply a moment after it starts, so the compute is
501
+ // waited for rather than given up on at the first refusal.
502
+ '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',
503
+ neonName("compute"),
504
+ ],
505
+ env: [{ name: "PGPASSWORD", valueFrom: { secretKeyRef: { name: databaseSecret(config), key: "password" } } }],
506
+ securityContext: restrictedContainer(),
507
+ resources: { requests: { cpu: "100m", memory: "128Mi" }, limits: { memory: "1Gi" } },
508
+ volumeMounts: [{ name: "backup", mountPath: "/backup" }],
509
+ };
510
+ const upload = {
511
+ name: "upload",
512
+ image: imageReference(config.neon.image),
513
+ imagePullPolicy: "IfNotPresent",
514
+ command: [
515
+ "/bin/sh",
516
+ "-c",
517
+ script("set -eu", 's3() { aws s3 --endpoint-url "$S3_ENDPOINT" "$@"; }', "stamp=$(date -u +%Y%m%dT%H%M%SZ)", 's3 cp /backup/alasio.dump "s3://$BUCKET/alasio-$stamp.dump"', "s3 ls \"s3://$BUCKET/\" | awk '{print $4}' | grep '^alasio-.*\\.dump$' | sort -r | tail -n +$((KEEP + 1)) \\", ' | while read -r old; do s3 rm "s3://$BUCKET/$old"; done'),
518
+ ],
519
+ env: [
520
+ { name: "S3_ENDPOINT", value: s3Endpoint(config) },
521
+ { name: "BUCKET", value: config.objectStore.buckets.backups },
522
+ { name: "KEEP", value: String(config.neon.backup.keep) },
523
+ { name: "AWS_DEFAULT_REGION", value: config.objectStore.external.region },
524
+ { name: "HOME", value: "/tmp" },
525
+ ],
526
+ envFrom: [{ secretRef: { name: neonName("s3-admin") } }],
527
+ securityContext: restrictedContainer(),
528
+ resources: { requests: { cpu: "50m", memory: "128Mi" }, limits: { memory: "512Mi" } },
529
+ volumeMounts: [{ name: "backup", mountPath: "/backup" }, { name: "tmp", mountPath: "/tmp" }],
530
+ };
531
+ return {
532
+ apiVersion: "batch/v1",
533
+ kind: "CronJob",
534
+ metadata: { name: neonName("backup"), namespace: NAMESPACE, labels: stackLabels(component) },
535
+ spec: {
536
+ schedule: config.neon.backup.schedule,
537
+ concurrencyPolicy: "Forbid",
538
+ successfulJobsHistoryLimit: 1,
539
+ failedJobsHistoryLimit: 3,
540
+ jobTemplate: {
541
+ spec: {
542
+ backoffLimit: 2,
543
+ activeDeadlineSeconds: 7200,
544
+ template: {
545
+ metadata: { labels: stackLabels(component) },
546
+ spec: {
547
+ restartPolicy: "Never",
548
+ ...stackPodSpec(config),
549
+ initContainers: [dump],
550
+ containers: [upload],
551
+ volumes: [{ name: "backup", emptyDir: {} }, { name: "tmp", emptyDir: {} }],
552
+ },
553
+ },
554
+ },
555
+ },
556
+ },
557
+ };
558
+ }
559
+ /**
560
+ * The stack's telemetry: every Neon service's, SeaweedFS's and the lake's Prometheus
561
+ * metrics, scraped and sent on as OTLP where alasio's telemetry goes, when it goes
562
+ * anywhere. Its pod carries its configuration's checksum, so it is replaced when that
563
+ * changes.
564
+ */
565
+ function collector(config, endpoint) {
566
+ const name = neonName("collector");
567
+ const component = "neon-collector";
568
+ const { telemetry } = config;
569
+ const safekeeper = neonName("safekeeper");
570
+ const targets = {
571
+ pageserver: [`${neonName("pageserver")}:9898`],
572
+ "storage-controller": [`${neonName("storage-controller")}:1234`],
573
+ "storage-broker": [`${neonName("storage-broker")}:50051`],
574
+ compute: [`${neonName("compute")}:3080`],
575
+ safekeeper: Array.from({ length: config.neon.safekeepers.replicas }, (_, index) => `${safekeeper}-${index}.${safekeeper}:7676`),
576
+ ...(config.objectStore.bundled.enabled ? { seaweedfs: [`${componentName("seaweedfs")}:9327`] } : {}),
577
+ ...(config.lake.enabled ? { lake: [`${componentName("lake")}:9464`] } : {}),
578
+ };
579
+ const collectorConfig = {
580
+ receivers: {
581
+ prometheus: {
582
+ config: {
583
+ scrape_configs: Object.keys(targets).sort().map((job) => ({ job_name: job, scrape_interval: "30s", static_configs: [{ targets: targets[job] }] })),
584
+ },
585
+ },
586
+ },
587
+ processors: { resource: { attributes: [{ key: "service.namespace", value: "alasio-neon", action: "upsert" }] }, batch: {} },
588
+ exporters: { otlphttp: { endpoint, ...(telemetry.headersSecret ? { headers: "${file:/etc/otelcol/headers/headers.yaml}" } : {}) } },
589
+ service: {
590
+ telemetry: { metrics: { level: "none" } },
591
+ pipelines: { metrics: { receivers: ["prometheus"], processors: ["resource", "batch"], exporters: ["otlphttp"] } },
592
+ },
593
+ };
594
+ const configMap = {
595
+ apiVersion: "v1",
596
+ kind: "ConfigMap",
597
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
598
+ data: { "config.yaml": goJson(collectorConfig, " ") },
599
+ };
600
+ const deployment = {
601
+ apiVersion: "apps/v1",
602
+ kind: "Deployment",
603
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
604
+ spec: {
605
+ replicas: 1,
606
+ selector: { matchLabels: selectorLabels(component) },
607
+ template: {
608
+ metadata: { labels: stackLabels(component), annotations: { "checksum/config": sha256(goJson(collectorConfig)) } },
609
+ spec: {
610
+ ...stackPodSpec(config),
611
+ containers: [{
612
+ name: "collector",
613
+ image: imageReference(config.neon.collector.image),
614
+ imagePullPolicy: "IfNotPresent",
615
+ args: ["--config=/etc/otelcol/config.yaml"],
616
+ securityContext: { ...restrictedContainer(), readOnlyRootFilesystem: true },
617
+ resources: config.neon.collector.resources,
618
+ volumeMounts: [
619
+ { name: "config", mountPath: "/etc/otelcol", readOnly: true },
620
+ ...(telemetry.headersSecret ? [{ name: "headers", mountPath: "/etc/otelcol/headers", readOnly: true }] : []),
621
+ ],
622
+ }],
623
+ volumes: [
624
+ { name: "config", configMap: { name } },
625
+ // The exporter's headers: a YAML map, under the key headersKey + ".yaml" in the
626
+ // headers Secret, since the collector takes headers as a map.
627
+ ...(telemetry.headersSecret
628
+ ? [{ name: "headers", secret: { secretName: telemetry.headersSecret, items: [{ key: `${telemetry.headersKey}.yaml`, path: "headers.yaml" }] } }]
629
+ : []),
630
+ ],
631
+ },
632
+ },
633
+ },
634
+ };
635
+ return [configMap, deployment];
636
+ }
637
+ /** Neon's objects, when it runs. */
638
+ export function neonObjects(config) {
639
+ const { neon } = config;
640
+ if (!neon.enabled)
641
+ return [];
642
+ const collectorEndpoint = neon.collector.otlpEndpoint || config.telemetry.otlpEndpoint;
643
+ return [
644
+ ...setup(config),
645
+ ...controllerDb(config),
646
+ ...storageBroker(config),
647
+ ...storageController(config),
648
+ ...safekeepers(config),
649
+ ...pageserver(config),
650
+ ...control(config),
651
+ ...compute(config),
652
+ ...(neon.backup.enabled ? [backup(config)] : []),
653
+ ...(neon.collector.enabled && collectorEndpoint ? collector(config, collectorEndpoint) : []),
654
+ ];
655
+ }
656
+ //# sourceMappingURL=neon.js.map