alasio 5.1.0 → 7.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 (132) hide show
  1. package/dist/cli/src/branches.js +53 -22
  2. package/dist/cli/src/branches.js.map +1 -1
  3. package/dist/cli/src/capacity.js +46 -0
  4. package/dist/cli/src/capacity.js.map +1 -0
  5. package/dist/cli/src/cluster/docker-engine.js +6 -4
  6. package/dist/cli/src/cluster/docker-engine.js.map +1 -1
  7. package/dist/cli/src/cluster/docker.js +49 -10
  8. package/dist/cli/src/cluster/docker.js.map +1 -1
  9. package/dist/cli/src/cluster/host.js +45 -19
  10. package/dist/cli/src/cluster/host.js.map +1 -1
  11. package/dist/cli/src/cluster/k3s.js +109 -9
  12. package/dist/cli/src/cluster/k3s.js.map +1 -1
  13. package/dist/cli/src/cluster/node.js +94 -50
  14. package/dist/cli/src/cluster/node.js.map +1 -1
  15. package/dist/cli/src/cluster/tar.js +43 -1
  16. package/dist/cli/src/cluster/tar.js.map +1 -1
  17. package/dist/cli/src/commands/branch.js +9 -4
  18. package/dist/cli/src/commands/branch.js.map +1 -1
  19. package/dist/cli/src/commands/restore-filer-store.js +247 -0
  20. package/dist/cli/src/commands/restore-filer-store.js.map +1 -0
  21. package/dist/cli/src/commands/status.js +141 -12
  22. package/dist/cli/src/commands/status.js.map +1 -1
  23. package/dist/cli/src/commands/uninstall.js +1 -1
  24. package/dist/cli/src/commands/uninstall.js.map +1 -1
  25. package/dist/cli/src/commands/up.js +42 -4
  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/commands.js +2 -1
  30. package/dist/cli/src/commands.js.map +1 -1
  31. package/dist/cli/src/config.js +13 -4
  32. package/dist/cli/src/config.js.map +1 -1
  33. package/dist/cli/src/cpu-baseline.js +97 -0
  34. package/dist/cli/src/cpu-baseline.js.map +1 -0
  35. package/dist/cli/src/install.js +44 -11
  36. package/dist/cli/src/install.js.map +1 -1
  37. package/dist/cli/src/kept-releases.js +59 -0
  38. package/dist/cli/src/kept-releases.js.map +1 -0
  39. package/dist/cli/src/kube/api.js +13 -5
  40. package/dist/cli/src/kube/api.js.map +1 -1
  41. package/dist/cli/src/kube/apply.js +116 -13
  42. package/dist/cli/src/kube/apply.js.map +1 -1
  43. package/dist/cli/src/kube/autoscalers.js +48 -0
  44. package/dist/cli/src/kube/autoscalers.js.map +1 -0
  45. package/dist/cli/src/kube/object-store-scale.js +116 -0
  46. package/dist/cli/src/kube/object-store-scale.js.map +1 -0
  47. package/dist/cli/src/kube/rollout.js +23 -0
  48. package/dist/cli/src/kube/rollout.js.map +1 -1
  49. package/dist/cli/src/manifests/admission.js +416 -0
  50. package/dist/cli/src/manifests/admission.js.map +1 -0
  51. package/dist/cli/src/manifests/agent-sandbox.js +6 -3
  52. package/dist/cli/src/manifests/agent-sandbox.js.map +1 -1
  53. package/dist/cli/src/manifests/alasio.js +165 -25
  54. package/dist/cli/src/manifests/alasio.js.map +1 -1
  55. package/dist/cli/src/manifests/branch.js +2 -1
  56. package/dist/cli/src/manifests/branch.js.map +1 -1
  57. package/dist/cli/src/manifests/cloudeventsources.eventing.keda.sh.json +219 -0
  58. package/dist/cli/src/manifests/clustercloudeventsources.eventing.keda.sh.json +218 -0
  59. package/dist/cli/src/manifests/clustertriggerauthentications.keda.sh.json +997 -0
  60. package/dist/cli/src/manifests/collector.js +92 -45
  61. package/dist/cli/src/manifests/collector.js.map +1 -1
  62. package/dist/cli/src/manifests/common.js +34 -8
  63. package/dist/cli/src/manifests/common.js.map +1 -1
  64. package/dist/cli/src/manifests/config.js +273 -35
  65. package/dist/cli/src/manifests/config.js.map +1 -1
  66. package/dist/cli/src/manifests/controllers.js +39 -0
  67. package/dist/cli/src/manifests/controllers.js.map +1 -0
  68. package/dist/cli/src/manifests/disruption.js +47 -0
  69. package/dist/cli/src/manifests/disruption.js.map +1 -0
  70. package/dist/cli/src/manifests/grafana.js +66 -10
  71. package/dist/cli/src/manifests/grafana.js.map +1 -1
  72. package/dist/cli/src/manifests/headroom.js +40 -0
  73. package/dist/cli/src/manifests/headroom.js.map +1 -0
  74. package/dist/cli/src/manifests/index.js +20 -6
  75. package/dist/cli/src/manifests/index.js.map +1 -1
  76. package/dist/cli/src/manifests/juicefs-csi.js +9 -6
  77. package/dist/cli/src/manifests/juicefs-csi.js.map +1 -1
  78. package/dist/cli/src/manifests/keda.js +271 -0
  79. package/dist/cli/src/manifests/keda.js.map +1 -0
  80. package/dist/cli/src/manifests/lake.js +13 -5
  81. package/dist/cli/src/manifests/lake.js.map +1 -1
  82. package/dist/cli/src/manifests/neon.js +196 -51
  83. package/dist/cli/src/manifests/neon.js.map +1 -1
  84. package/dist/cli/src/manifests/network-policies.js +177 -28
  85. package/dist/cli/src/manifests/network-policies.js.map +1 -1
  86. package/dist/cli/src/manifests/node-agent.js +107 -0
  87. package/dist/cli/src/manifests/node-agent.js.map +1 -0
  88. package/dist/cli/src/manifests/object-store.js +543 -82
  89. package/dist/cli/src/manifests/object-store.js.map +1 -1
  90. package/dist/cli/src/manifests/priorities.js +31 -0
  91. package/dist/cli/src/manifests/priorities.js.map +1 -0
  92. package/dist/cli/src/manifests/scaledjobs.keda.sh.json +7507 -0
  93. package/dist/cli/src/manifests/scaledobjects.keda.sh.json +564 -0
  94. package/dist/cli/src/manifests/scalers.js +24 -0
  95. package/dist/cli/src/manifests/scalers.js.map +1 -0
  96. package/dist/cli/src/manifests/sizing.js +40 -0
  97. package/dist/cli/src/manifests/sizing.js.map +1 -0
  98. package/dist/cli/src/manifests/triggerauthentications.keda.sh.json +992 -0
  99. package/dist/cli/src/manifests/valkey.js +63 -35
  100. package/dist/cli/src/manifests/valkey.js.map +1 -1
  101. package/dist/cli/src/manifests/verticalpodautoscalercheckpoints.autoscaling.k8s.io.json +250 -0
  102. package/dist/cli/src/manifests/verticalpodautoscalers.autoscaling.k8s.io.json +837 -0
  103. package/dist/cli/src/manifests/vpa.js +220 -0
  104. package/dist/cli/src/manifests/vpa.js.map +1 -0
  105. package/dist/cli/src/manifests/workspace-storage.js +12 -2
  106. package/dist/cli/src/manifests/workspace-storage.js.map +1 -1
  107. package/dist/cli/src/object-store.js +57 -0
  108. package/dist/cli/src/object-store.js.map +1 -0
  109. package/dist/cli/src/release.js +6 -6
  110. package/dist/cli/src/target.js +1 -1
  111. package/dist/cli/src/target.js.map +1 -1
  112. package/dist/cluster/node/pins.json +6 -2
  113. package/dist/neon/control/branches.js +31 -12
  114. package/dist/neon/control/branches.js.map +1 -1
  115. package/dist/neon/control/endpoints.js +108 -0
  116. package/dist/neon/control/endpoints.js.map +1 -0
  117. package/dist/neon/control/scram.js +13 -1
  118. package/dist/neon/control/scram.js.map +1 -1
  119. package/dist/neon/control/secrets.js +52 -8
  120. package/dist/neon/control/secrets.js.map +1 -1
  121. package/dist/src/kube/busy-label.js +13 -0
  122. package/dist/src/kube/busy-label.js.map +1 -0
  123. package/dist/src/kube/config.js +30 -4
  124. package/dist/src/kube/config.js.map +1 -1
  125. package/dist/src/kube/sandbox-shape.js +98 -0
  126. package/dist/src/kube/sandbox-shape.js.map +1 -0
  127. package/dist/src/node-agent/names.js +74 -0
  128. package/dist/src/node-agent/names.js.map +1 -0
  129. package/dist/src/sandbox/names.js +44 -0
  130. package/dist/src/sandbox/names.js.map +1 -0
  131. package/package.json +2 -2
  132. package/dist/cluster/node/config-v3.toml.tmpl +0 -12
@@ -1,20 +1,52 @@
1
1
  import { HISTORY_DAYS } from "../../../neon/control/branches.js";
2
2
  import { imageReference } from "../images.js";
3
- import { claimSpec, componentName, NAMESPACE, neonName, restrictedContainer, script, selectorLabels, stackLabels, stackPodSpec } from "./common.js";
4
- /** Every port its servers advertise themselves on by the Service's name, gRPC's each the HTTP port plus 10000. */
5
- const PORTS = [
6
- { name: "s3", port: 8333 },
7
- { name: "s3-grpc", port: 18333 },
8
- { name: "master", port: 9333 },
9
- { name: "master-grpc", port: 19333 },
10
- { name: "volume", port: 8080 },
11
- { name: "volume-grpc", port: 18080 },
12
- { name: "filer", port: 8888 },
13
- { name: "filer-grpc", port: 18888 },
14
- { name: "metrics", port: 9327 },
15
- ];
16
- /** The ports by which the server reaches itself: its master, volume and filer servers'. */
17
- const PEER_PORTS = PORTS.filter(({ name }) => /^(master|volume|filer)/u.test(name));
3
+ import { claimSpec, componentName, labels, NAMESPACE, neonName, replicasOf, restrictedContainer, sandboxSnapshots, scaled, script, selectorLabels, stackLabels, stackPodSpec } from "./common.js";
4
+ import { filerStoreMaxmemoryBytes } from "./config.js";
5
+ import { replicaBudget, spread } from "./disruption.js";
6
+ import { priority } from "./priorities.js";
7
+ import { cpuUtilization, scaledObject } from "./scalers.js";
8
+ import { verticalPodAutoscaler } from "./sizing.js";
9
+ import { VALKEY_PORT, valkeyObjects, valkeyPassword, valkeyPod } from "./valkey.js";
10
+ /** The gateways' name, their Service's, by which the stack reaches the store. */
11
+ export const GATEWAY = componentName("seaweedfs");
12
+ export const MASTER = componentName("seaweedfs-master");
13
+ export const VOLUME_SERVER = componentName("seaweedfs-volume");
14
+ /** The filers' StatefulSet's name, and their Service's, by which a client reaches any of them. */
15
+ export const FILER = componentName("seaweedfs-filer");
16
+ /** The filers' headless Service, which names each, as they reach each other and the gateways reach them. */
17
+ export const FILER_PEERS = componentName("seaweedfs-filer-peers");
18
+ /** The filer store's name: its Service's and its StatefulSet's. */
19
+ export const FILER_STORE = componentName("seaweedfs-filer-store");
20
+ /**
21
+ * The filer store's users, each with its password in a Secret of its own, its default user
22
+ * off: the filers', to the keys under their prefix alone, and of no command that could
23
+ * take down or reconfigure the store; the collector's, which reads INFO and PINGs, as its
24
+ * readiness probe does; and the backups', which take a snapshot of the store as a replica
25
+ * would, and nothing else.
26
+ */
27
+ export const FILER_STORE_USERS = {
28
+ filer: { name: "filer", secret: `${FILER_STORE}-filer`, rules: ["~seaweedfs:*", "+@all", "-@dangerous"] },
29
+ collector: { name: "collector", secret: `${FILER_STORE}-collector`, rules: ["+info", "+ping"], probes: true },
30
+ backup: { name: "backup", secret: `${FILER_STORE}-backup`, rules: ["+replconf", "+sync", "+psync", "+ping"] },
31
+ };
32
+ /** The components of the store's parts, by which their pods are told. */
33
+ export const COMPONENTS = { gateway: "seaweedfs", master: "seaweedfs-master", volumeServer: "seaweedfs-volume", filer: "seaweedfs-filer", filerStore: "seaweedfs-filer-store" };
34
+ /** The component of the lifecycle pass's pods. */
35
+ export const LIFECYCLE_COMPONENT = "seaweedfs-lifecycle";
36
+ /** The component of the filer store's backups' pods, and the name of the volume they are kept on. */
37
+ export const BACKUP_COMPONENT = "seaweedfs-filer-store-backup";
38
+ export const BACKUPS = componentName("seaweedfs-filer-store-backups");
39
+ /** The port each part serves its Prometheus metrics on. */
40
+ export const METRICS_PORT = 9327;
41
+ /** The key prefix of the filers' entries in their store. */
42
+ const KEY_PREFIX = "seaweedfs:";
43
+ /** Each part's ports: its HTTP port's, and its gRPC port's, the HTTP port plus 10000, as SeaweedFS reaches them. */
44
+ const PORTS = {
45
+ master: [{ name: "http", port: 9333 }, { name: "grpc", port: 19333 }],
46
+ volumeServer: [{ name: "http", port: 8080 }, { name: "grpc", port: 18080 }],
47
+ filer: [{ name: "http", port: 8888 }, { name: "grpc", port: 18888 }],
48
+ gateway: [{ name: "s3", port: 8333 }, { name: "s3-grpc", port: 18333 }],
49
+ };
18
50
  /**
19
51
  * The days back to which Neon's time-travel recovery can put the pageserver's objects:
20
52
  * the history Neon keeps (HISTORY_DAYS), and a day more to notice a deletion that should
@@ -43,7 +75,7 @@ const WAL_SEGMENT_BYTES = 16 * 1024 * 1024;
43
75
  * An operator's store (objectStore.external) is given none of this: its operator gives
44
76
  * Neon's bucket these rules.
45
77
  */
46
- const LIFECYCLE = {
78
+ const NEON_LIFECYCLE = {
47
79
  Rules: [
48
80
  {
49
81
  ID: "expire-noncurrent",
@@ -67,84 +99,274 @@ const LIFECYCLE = {
67
99
  },
68
100
  ],
69
101
  };
70
- /** The server, and the Services by which it is reached and reaches itself. */
71
- function server(config) {
72
- const name = componentName("seaweedfs");
73
- const peers = `${name}-peers`;
74
- const { bundled, buckets } = config.objectStore;
75
- const made = [buckets.neon, buckets.lake, buckets.backups, ...(config.workspaceStorage.enabled ? [buckets.workspaces] : [])];
102
+ /**
103
+ * The lifecycle rule of the snapshots bucket. A node agent puts a file larger than a
104
+ * part in parts (src/node-agent/s3.ts), and aborts an upload that fails; one it could not
105
+ * abort, ended mid-upload, leaves the parts it had sent, which this aborts after a day,
106
+ * the least S3 allows. An external store is given no rule by alasio: its operator gives
107
+ * the bucket this one.
108
+ */
109
+ const SNAPSHOTS_LIFECYCLE = {
110
+ Rules: [{ ID: "abort-incomplete-uploads", Status: "Enabled", Filter: { Prefix: "" }, AbortIncompleteMultipartUpload: { DaysAfterInitiation: 1 } }],
111
+ };
112
+ /** The address of the pod `ordinal` of the StatefulSet `name`, by its headless Service `service`, at `port`. */
113
+ const podAddress = (name, service, ordinal, port) => `${name}-${ordinal}.${service}.${NAMESPACE}.svc.cluster.local:${port}`;
114
+ /** The addresses of the `count` pods of the StatefulSet `name`, by `service`, at `port`, as SeaweedFS takes a list of them. */
115
+ const podAddresses = (name, service, count, port) => Array.from({ length: count }, (_, ordinal) => podAddress(name, service, ordinal, port)).join(",");
116
+ /** The masters, as the other parts reach them. */
117
+ const masters = ({ objectStore }) => podAddresses(MASTER, MASTER, objectStore.bundled.masters.scaling.min, 9333);
118
+ /** A pod's name and namespace, by which a part names itself to the others. */
119
+ const POD_ENV = [
120
+ { name: "POD_NAME", valueFrom: { fieldRef: { fieldPath: "metadata.name" } } },
121
+ { name: "POD_NAMESPACE", valueFrom: { fieldRef: { fieldPath: "metadata.namespace" } } },
122
+ ];
123
+ /** The address a pod of a StatefulSet names itself by, its own in its headless Service `service`. */
124
+ const ownAddress = (service) => `-ip=$(POD_NAME).${service}.$(POD_NAMESPACE).svc.cluster.local`;
125
+ /** The headless Service `name` of `component`'s pods, on `ports`, which names each pod, ready or not. */
126
+ function headless(name, component, ports) {
127
+ return {
128
+ apiVersion: "v1",
129
+ kind: "Service",
130
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
131
+ spec: { clusterIP: "None", publishNotReadyAddresses: true, selector: selectorLabels(component), ports: [...ports, { name: "metrics", port: METRICS_PORT }] },
132
+ };
133
+ }
134
+ /** The StatefulSet `name` of `component`, of `scale`, running `containers`, as every part of the store's is: on the floor, above it where its claims hold it to a node, as the data stack's user. */
135
+ function partStatefulSet(config, name, component, scale, pod, storage, { service = name, ordered = false, strictly = false } = {}) {
136
+ return {
137
+ apiVersion: "apps/v1",
138
+ kind: "StatefulSet",
139
+ metadata: { name, namespace: NAMESPACE, labels: stackLabels(component) },
140
+ spec: {
141
+ serviceName: service,
142
+ ...replicasOf(scale),
143
+ podManagementPolicy: ordered ? "OrderedReady" : "Parallel",
144
+ selector: { matchLabels: selectorLabels(component) },
145
+ template: {
146
+ metadata: { labels: stackLabels(component) },
147
+ spec: {
148
+ ...priority(storage ? "bound" : "floor"),
149
+ ...stackPodSpec(config),
150
+ ...spread(component, scale, strictly),
151
+ terminationGracePeriodSeconds: 30,
152
+ ...(pod.initContainers ? { initContainers: pod.initContainers } : {}),
153
+ containers: pod.containers,
154
+ ...(pod.volumes ? { volumes: pod.volumes } : {}),
155
+ },
156
+ },
157
+ ...(storage ? { volumeClaimTemplates: [{ metadata: { name: "data" }, spec: claimSpec(storage) }] } : {}),
158
+ },
159
+ };
160
+ }
161
+ /** The container of a part, `name`, running `weed` with `args`, probed at `path` on its HTTP port. */
162
+ function partContainer(config, name, args, path, port, resources) {
163
+ const probe = { httpGet: { path, port } };
164
+ return {
165
+ name,
166
+ image: imageReference(config.objectStore.bundled.image),
167
+ imagePullPolicy: "IfNotPresent",
168
+ args: [...args, `-metricsPort=${METRICS_PORT}`],
169
+ env: POD_ENV,
170
+ ports: [{ name: "http", containerPort: port }, { name: "grpc", containerPort: port + 10_000 }, { name: "metrics", containerPort: METRICS_PORT }],
171
+ readinessProbe: { ...probe, periodSeconds: 5 },
172
+ livenessProbe: { ...probe, initialDelaySeconds: 30, periodSeconds: 10, failureThreshold: 6 },
173
+ securityContext: restrictedContainer(),
174
+ ...(resources ? { resources } : {}),
175
+ };
176
+ }
177
+ /** The masters: of their state, a volume each; of three, in Raft, each naming the others. */
178
+ function masterObjects(config) {
179
+ const { bundled } = config.objectStore;
180
+ const { scaling } = bundled.masters;
181
+ const args = [
182
+ "master",
183
+ "-mdir=/data",
184
+ ownAddress(MASTER),
185
+ "-ip.bind=0.0.0.0",
186
+ // One alone needs no quorum: none, it is the leader as it starts.
187
+ `-peers=${scaling.min > 1 ? masters(config) : "none"}`,
188
+ "-volumeSizeLimitMB=1024",
189
+ `-defaultReplication=${bundled.replication}`,
190
+ // A volume is compacted once a tenth of it is deleted, not a third as by default:
191
+ // Neon's objects all expire within days, so every volume comes to hold deleted ones,
192
+ // and the store would keep up to a third of its disk dead. A compaction copies the
193
+ // volume's live rest, at about half a gigabyte a second.
194
+ "-garbageThreshold=0.1",
195
+ "-telemetry=false",
196
+ ];
197
+ const container = { ...partContainer(config, "master", args, "/cluster/status", 9333, bundled.masters.resources), volumeMounts: [{ name: "data", mountPath: "/data" }] };
198
+ const statefulSet = partStatefulSet(config, MASTER, COMPONENTS.master, scaling, { containers: [container] }, bundled.masters.storage);
199
+ return [headless(MASTER, COMPONENTS.master, PORTS.master), statefulSet, ...replicaBudget(COMPONENTS.master, NAMESPACE, scaling), verticalPodAutoscaler(statefulSet, "Recommended")];
200
+ }
201
+ /**
202
+ * The volume servers, each keeping its volumes on a volume of its own, in one data center
203
+ * and rack, so a copy goes to another server; and, where copies are kept, each on a node
204
+ * of its own while there are as many, so a copy is on another node, a server left
205
+ * unplaced rather than put beside another.
206
+ */
207
+ function volumeServerObjects(config) {
208
+ const { bundled } = config.objectStore;
209
+ const { volumeServers } = bundled;
210
+ const args = [
211
+ "volume",
212
+ "-dir=/data",
213
+ `-max=${volumeServers.volumes}`,
214
+ `-minFreeSpace=${volumeServers.minFreeSpace}`,
215
+ `-master=${masters(config)}`,
216
+ ownAddress(VOLUME_SERVER),
217
+ "-ip.bind=0.0.0.0",
218
+ "-dataCenter=dc1",
219
+ "-rack=rack1",
220
+ ];
221
+ const container = { ...partContainer(config, "volume", args, "/status", 8080, volumeServers.resources), volumeMounts: [{ name: "data", mountPath: "/data" }] };
222
+ const statefulSet = partStatefulSet(config, VOLUME_SERVER, COMPONENTS.volumeServer, volumeServers.scaling, { containers: [container] }, volumeServers.storage, {
223
+ strictly: bundled.replication !== "000",
224
+ });
225
+ return [
226
+ headless(VOLUME_SERVER, COMPONENTS.volumeServer, PORTS.volumeServer),
227
+ statefulSet,
228
+ ...replicaBudget(COMPONENTS.volumeServer, NAMESPACE, volumeServers.scaling),
229
+ verticalPodAutoscaler(statefulSet, "Recommended"),
230
+ ];
231
+ }
232
+ /** The filers' configuration: their store, the filer store, as its user `filer`, whose password each is given as WEED_REDIS2_PASSWORD. */
233
+ export function filerToml() {
234
+ return [
235
+ "[redis2]",
236
+ "enabled = true",
237
+ `address = "${FILER_STORE}:${VALKEY_PORT}"`,
238
+ `username = "${FILER_STORE_USERS.filer.name}"`,
239
+ "database = 0",
240
+ `keyPrefix = "${KEY_PREFIX}"`,
241
+ "superLargeDirectories = []",
242
+ "",
243
+ ].join("\n");
244
+ }
245
+ /**
246
+ * The filers, started one after another on the filer store, each once the store has
247
+ * loaded what it keeps and a volume server has registered with the masters.
248
+ */
249
+ function filerObjects(config) {
250
+ const { bundled } = config.objectStore;
251
+ const { filers } = bundled;
76
252
  const image = imageReference(bundled.image);
77
- const ready = { exec: { command: ["test", "-f", "/tmp/ready"] }, periodSeconds: 5 };
253
+ const configMap = { apiVersion: "v1", kind: "ConfigMap", metadata: { name: FILER, namespace: NAMESPACE, labels: stackLabels(COMPONENTS.filer) }, data: { "filer.toml": filerToml() } };
254
+ const waiting = (name, waitImage, line, env = []) => ({
255
+ name: `wait-for-${name}`,
256
+ image: waitImage,
257
+ imagePullPolicy: "IfNotPresent",
258
+ command: ["/bin/sh", "-c", line],
259
+ ...(env.length > 0 ? { env } : {}),
260
+ securityContext: restrictedContainer(),
261
+ resources: { requests: { cpu: "10m", memory: "16Mi" }, limits: { memory: "64Mi" } },
262
+ });
263
+ const initContainers = [
264
+ // Answers PONG once it has loaded its append-only file, LOADING before.
265
+ waiting("filer-store", imageReference(bundled.filerStore.image), `until valkey-cli -h ${FILER_STORE} --user ${FILER_STORE_USERS.filer.name} ping 2>/dev/null | grep -q PONG; do sleep 2; done`, [{ name: "VALKEYCLI_AUTH", valueFrom: valkeyPassword(FILER_STORE_USERS.filer.secret) }]),
266
+ waiting("volume-server", image, `until wget -q -O - http://${MASTER}:9333/dir/status | grep -q '"DataNodes":\\[{'; do sleep 2; done`),
267
+ ];
268
+ const args = ["filer", `-master=${masters(config)}`, ownAddress(FILER_PEERS), "-ip.bind=0.0.0.0", "-defaultStoreDir=/tmp"];
269
+ const container = {
270
+ ...partContainer(config, "filer", args, "/healthz", 8888, filers.resources),
271
+ env: [...POD_ENV, { name: "WEED_REDIS2_PASSWORD", valueFrom: valkeyPassword(FILER_STORE_USERS.filer.secret) }],
272
+ volumeMounts: [{ name: "config", mountPath: "/etc/seaweedfs", readOnly: true }, { name: "tmp", mountPath: "/tmp" }],
273
+ };
274
+ const statefulSet = partStatefulSet(config, FILER, COMPONENTS.filer, filers.scaling, { initContainers, containers: [container], volumes: [{ name: "config", configMap: { name: FILER } }, { name: "tmp", emptyDir: {} }] }, undefined, { service: FILER_PEERS, ordered: true });
275
+ // Any filer, for the shells that need one: the buckets' and the lifecycle pass's.
78
276
  const service = {
79
277
  apiVersion: "v1",
80
278
  kind: "Service",
81
- metadata: { name, namespace: NAMESPACE, labels: stackLabels("seaweedfs") },
82
- spec: { selector: selectorLabels("seaweedfs"), ports: PORTS },
279
+ metadata: { name: FILER, namespace: NAMESPACE, labels: stackLabels(COMPONENTS.filer) },
280
+ spec: { selector: selectorLabels(COMPONENTS.filer), ports: PORTS.filer },
83
281
  };
84
- // The server's own stable name, which it advertises and reaches itself by (its master
85
- // elects itself leader through it) before it is ready, as the Service above admits
86
- // clients only once its buckets exist.
87
- const peersService = {
282
+ return [
283
+ configMap,
284
+ headless(FILER_PEERS, COMPONENTS.filer, PORTS.filer),
285
+ service,
286
+ statefulSet,
287
+ ...replicaBudget(COMPONENTS.filer, NAMESPACE, filers.scaling),
288
+ verticalPodAutoscaler(statefulSet, "InPlace"),
289
+ ];
290
+ }
291
+ /** The filer store: a Valkey of the filers' own, its memory sized for its entries. */
292
+ function filerStoreObjects(config) {
293
+ const { filerStore } = config.objectStore.bundled;
294
+ const maxmemory = `${filerStoreMaxmemoryBytes(filerStore.entries) / 2 ** 20}mb`;
295
+ const { filer, collector, backup } = FILER_STORE_USERS;
296
+ return valkeyObjects(config, {
297
+ name: FILER_STORE,
298
+ component: COMPONENTS.filerStore,
299
+ image: filerStore.image,
300
+ storage: filerStore.storage,
301
+ resources: filerStore.resources,
302
+ maxmemory,
303
+ users: [filer, collector, backup],
304
+ });
305
+ }
306
+ /**
307
+ * The S3 gateways, on the filers, each making the buckets and their lifecycle rules as
308
+ * it starts, in sidecars, and ready only once they are made.
309
+ */
310
+ function gatewayObjects(config) {
311
+ const { bundled, buckets } = config.objectStore;
312
+ const { scaling } = bundled.gateways;
313
+ const snapshots = sandboxSnapshots(config);
314
+ const made = [buckets.neon, buckets.lake, buckets.backups, ...(config.workspaceStorage.enabled ? [buckets.workspaces] : []), ...(snapshots ? [buckets.snapshots] : [])];
315
+ // Each bucket's rules given as its admin: Neon's as the admin's, the snapshots' as their own
316
+ // identity's, as nothing else may write that bucket (neon/control/secrets.ts s3Identities).
317
+ const lifecycles = [
318
+ { bucket: buckets.neon, rules: NEON_LIFECYCLE, as: "ADMIN" },
319
+ ...(snapshots ? [{ bucket: buckets.snapshots, rules: SNAPSHOTS_LIFECYCLE, as: "SNAPSHOTS" }] : []),
320
+ ];
321
+ const image = imageReference(bundled.image);
322
+ const ready = { exec: { command: ["test", "-f", "/tmp/ready"] }, periodSeconds: 5 };
323
+ const service = {
88
324
  apiVersion: "v1",
89
325
  kind: "Service",
90
- metadata: { name: peers, namespace: NAMESPACE, labels: stackLabels("seaweedfs") },
91
- spec: { clusterIP: "None", publishNotReadyAddresses: true, selector: selectorLabels("seaweedfs"), ports: PEER_PORTS },
326
+ metadata: { name: GATEWAY, namespace: NAMESPACE, labels: stackLabels(COMPONENTS.gateway) },
327
+ spec: { selector: selectorLabels(COMPONENTS.gateway), ports: [...PORTS.gateway, { name: "metrics", port: METRICS_PORT }] },
92
328
  };
93
- const statefulSet = {
329
+ const deployment = {
94
330
  apiVersion: "apps/v1",
95
- kind: "StatefulSet",
96
- metadata: { name, namespace: NAMESPACE, labels: stackLabels("seaweedfs") },
331
+ kind: "Deployment",
332
+ metadata: { name: GATEWAY, namespace: NAMESPACE, labels: stackLabels(COMPONENTS.gateway) },
97
333
  spec: {
98
- serviceName: peers,
99
- replicas: 1,
100
- selector: { matchLabels: selectorLabels("seaweedfs") },
334
+ ...replicasOf(scaling),
335
+ selector: { matchLabels: selectorLabels(COMPONENTS.gateway) },
101
336
  template: {
102
- metadata: { labels: stackLabels("seaweedfs") },
337
+ metadata: { labels: stackLabels(COMPONENTS.gateway) },
103
338
  spec: {
339
+ ...priority("floor"),
104
340
  ...stackPodSpec(config),
341
+ ...spread(COMPONENTS.gateway, scaling),
105
342
  terminationGracePeriodSeconds: 30,
106
343
  containers: [
107
344
  {
108
- name: "seaweedfs",
345
+ name: "s3",
109
346
  image,
110
347
  imagePullPolicy: "IfNotPresent",
111
348
  args: [
112
- "server",
113
- `-ip=$(POD_NAME).${peers}.$(POD_NAMESPACE).svc.cluster.local`,
349
+ "s3",
350
+ `-filer=${podAddresses(FILER, FILER_PEERS, bundled.filers.scaling.min, 8888)}`,
114
351
  "-ip.bind=0.0.0.0",
115
- "-dir=/data",
116
- `-volume.max=${bundled.volumes}`,
117
- "-master.volumeSizeLimitMB=1024",
118
- "-master.defaultReplication=000",
119
- // Its master compacts a volume once a tenth of it is deleted, not a third
120
- // as by default: Neon's objects all expire within days, so every volume
121
- // comes to hold deleted ones, and the store would keep up to a third of
122
- // its disk dead. A compaction copies the volume's live rest, at about half
123
- // a gigabyte a second.
124
- "-master.garbageThreshold=0.1",
125
- "-master.telemetry=false",
126
- `-volume.minFreeSpace=${bundled.minFreeSpace}`,
127
- "-filer",
128
- "-s3",
129
- "-s3.config=/etc/seaweedfs/s3.json",
130
- "-s3.allowDeleteBucketNotEmpty=false",
131
- "-s3.autoCreateBucket=false",
132
- "-metricsPort=9327",
133
- ],
134
- env: [
135
- { name: "POD_NAME", valueFrom: { fieldRef: { fieldPath: "metadata.name" } } },
136
- { name: "POD_NAMESPACE", valueFrom: { fieldRef: { fieldPath: "metadata.namespace" } } },
352
+ "-config=/etc/seaweedfs/s3.json",
353
+ "-allowDeleteBucketNotEmpty=false",
354
+ "-autoCreateBucket=false",
355
+ `-metricsPort=${METRICS_PORT}`,
137
356
  ],
138
- ports: PORTS.map(({ name, port }) => ({ name, containerPort: port })),
357
+ ports: [...PORTS.gateway.map(({ name, port }) => ({ name, containerPort: port })), { name: "metrics", containerPort: METRICS_PORT }],
139
358
  readinessProbe: { httpGet: { path: "/healthz", port: "s3" }, periodSeconds: 5 },
140
359
  livenessProbe: { httpGet: { path: "/healthz", port: "s3" }, initialDelaySeconds: 30, periodSeconds: 10, failureThreshold: 6 },
141
360
  securityContext: restrictedContainer(),
142
- resources: bundled.resources,
143
- volumeMounts: [{ name: "data", mountPath: "/data" }, { name: "identities", mountPath: "/etc/seaweedfs", readOnly: true }],
361
+ resources: bundled.gateways.resources,
362
+ volumeMounts: [{ name: "identities", mountPath: "/etc/seaweedfs", readOnly: true }],
144
363
  },
145
- // Makes the buckets, idempotently, each time the server starts: Neon's versioned
146
- // (its time-travel recovery needs that), writes fsynced. The pod is ready only
147
- // once they exist, so nothing that stores in them starts before.
364
+ // Makes the buckets, idempotently, as the gateway starts: Neon's versioned (its
365
+ // time-travel recovery needs that), writes fsynced. One a gateway beside it has
366
+ // made at once is found made. A bucket whose objects the volumes hold, by its
367
+ // collection, while the filers know no such bucket, is not made anew, empty: the
368
+ // filer store has lost what it kept, which `alasio restore-filer-store` puts back instead,
369
+ // and the gateway is not ready until it is.
148
370
  {
149
371
  name: "buckets",
150
372
  image,
@@ -152,14 +374,16 @@ function server(config) {
152
374
  command: [
153
375
  "/bin/sh",
154
376
  "-c",
155
- script("set -e", "shell() { printf '%s\\n' \"$1\" | weed shell -master=127.0.0.1:9333 -filer=127.0.0.1:8888; }", "until wget -q -O /dev/null http://127.0.0.1:8333/healthz; do sleep 2; done", "shell 'fs.configure -locationPrefix=/buckets/ -volumeGrowthCount=1 -fsync -apply'", `for bucket in ${made.join(" ")}; do`, " shell 's3.bucket.list' | grep -qw \"$bucket\" || shell \"s3.bucket.create -name $bucket\"", "done", `shell 's3.bucket.versioning -name ${buckets.neon} -enable'`, `shell 's3.bucket.versioning -name ${buckets.neon}' | grep -qi enabled`, "touch /tmp/ready", "exec sleep infinity"),
377
+ script("set -e", `shell() { printf '%s\\n' "$1" | weed shell -master=${masters(config)} -filer=${FILER}:8888; }`,
378
+ // A bucket, by its name exactly, as each line of the list starts with one.
379
+ "listed() { shell 's3.bucket.list' | awk '{print $1}' | grep -qxF \"$1\"; }", "until wget -q -O /dev/null http://127.0.0.1:8333/healthz; do sleep 2; done", "shell 'fs.configure -locationPrefix=/buckets/ -volumeGrowthCount=1 -fsync -apply'", `for bucket in ${made.join(" ")}; do`, " if ! listed \"$bucket\"; then", " if shell 'volume.list' | grep -q \"Collection:$bucket,\"; then", " echo \"the store's volumes hold objects of the bucket $bucket, which its filers do not know: their store has lost what it kept; alasio restore-filer-store puts it back from its backups, rather than make $bucket anew, empty\" >&2", " exit 1", " fi", " shell \"s3.bucket.create -name $bucket\"", " fi", " listed \"$bucket\"", "done", `shell 's3.bucket.versioning -name ${buckets.neon} -enable'`, `shell 's3.bucket.versioning -name ${buckets.neon}' | grep -qi enabled`, "touch /tmp/ready", "exec sleep infinity"),
156
380
  ],
157
381
  readinessProbe: ready,
158
382
  securityContext: restrictedContainer(),
159
383
  resources: { requests: { cpu: "10m", memory: "32Mi" }, limits: { memory: "256Mi" } },
160
384
  volumeMounts: [{ name: "shell-tmp", mountPath: "/tmp" }],
161
385
  },
162
- // Gives Neon's bucket its lifecycle rules once it exists.
386
+ // Gives each bucket with lifecycle rules its rules once it exists.
163
387
  {
164
388
  name: "lifecycle",
165
389
  image: imageReference(config.neon.image),
@@ -167,10 +391,25 @@ function server(config) {
167
391
  command: [
168
392
  "/bin/sh",
169
393
  "-c",
170
- script("set -e", `until aws s3api --endpoint-url http://127.0.0.1:8333 head-bucket --bucket ${buckets.neon} 2>/dev/null; do sleep 2; done`, `aws s3api --endpoint-url http://127.0.0.1:8333 put-bucket-lifecycle-configuration --bucket ${buckets.neon} --lifecycle-configuration '${JSON.stringify(LIFECYCLE)}'`, "touch /tmp/ready", "exec sleep infinity"),
394
+ script("set -e", ...lifecycles.flatMap(({ bucket, rules, as }) => {
395
+ const aws = `AWS_ACCESS_KEY_ID="$${as}_KEY_ID" AWS_SECRET_ACCESS_KEY="$${as}_SECRET" aws s3api --endpoint-url http://127.0.0.1:8333`;
396
+ return [
397
+ `until ${aws} head-bucket --bucket ${bucket} 2>/dev/null; do sleep 2; done`,
398
+ `${aws} put-bucket-lifecycle-configuration --bucket ${bucket} --lifecycle-configuration '${JSON.stringify(rules)}'`,
399
+ ];
400
+ }), "touch /tmp/ready", "exec sleep infinity"),
401
+ ],
402
+ env: [
403
+ { name: "AWS_DEFAULT_REGION", value: "us-east-1" },
404
+ { name: "HOME", value: "/tmp" },
405
+ ...lifecycles.flatMap(({ as }) => {
406
+ const secret = as === "ADMIN" ? neonName("s3-admin") : componentName("snapshots");
407
+ return [
408
+ { name: `${as}_KEY_ID`, valueFrom: { secretKeyRef: { name: secret, key: "AWS_ACCESS_KEY_ID" } } },
409
+ { name: `${as}_SECRET`, valueFrom: { secretKeyRef: { name: secret, key: "AWS_SECRET_ACCESS_KEY" } } },
410
+ ];
411
+ }),
171
412
  ],
172
- env: [{ name: "AWS_DEFAULT_REGION", value: "us-east-1" }, { name: "HOME", value: "/tmp" }],
173
- envFrom: [{ secretRef: { name: neonName("s3-admin") } }],
174
413
  readinessProbe: ready,
175
414
  securityContext: restrictedContainer(),
176
415
  resources: { requests: { cpu: "10m", memory: "64Mi" }, limits: { memory: "256Mi" } },
@@ -184,22 +423,41 @@ function server(config) {
184
423
  ],
185
424
  },
186
425
  },
187
- volumeClaimTemplates: [{ metadata: { name: "data" }, spec: claimSpec(bundled.storage) }],
188
426
  },
189
427
  };
190
- return [service, peersService, statefulSet];
428
+ return [
429
+ service,
430
+ deployment,
431
+ ...scaledObject(deployment, scaling, [cpuUtilization(80)]),
432
+ ...replicaBudget(COMPONENTS.gateway, NAMESPACE, scaling),
433
+ // Of memory alone where KEDA scales the gateways on their CPU, as the two would chase each other.
434
+ verticalPodAutoscaler(deployment, "InPlace", scaled(scaling) ? ["memory"] : undefined),
435
+ ];
191
436
  }
192
437
  /**
193
438
  * SeaweedFS applies lifecycle rules only when asked: a pass every hour, deleting through
194
- * the S3 server's gRPC port. A pass replays the filer's log of changes from where each
439
+ * the gateways' gRPC port. A pass replays the filers' log of changes from where each
195
440
  * shard's last stopped, which is the oldest change with an expiry not yet due; so it reads
196
441
  * to now (`-events 0`), not a count of changes, which would hold a short rule's expiries
197
442
  * behind a long one's until that one's came due. It stops at its runtime, well before the
198
443
  * next, and the one after it goes on from there.
444
+ *
445
+ * The pass then sets each volume's replication to the store's, which differs where the
446
+ * store's was changed since the volume was made, and, where copies are kept, copies again
447
+ * each volume with fewer than that, as a volume server lost, or come back empty, or a
448
+ * replication raised leaves it; holding the shell's lock while it does, which one shell
449
+ * holds at a time.
199
450
  */
200
451
  function lifecyclePass(config) {
201
- const name = componentName("seaweedfs");
202
- const component = "seaweedfs-lifecycle";
452
+ const component = LIFECYCLE_COMPONENT;
453
+ const { replication } = config.objectStore.bundled;
454
+ const commands = [
455
+ `s3.lifecycle.run-shard -shards 0-15 -s3 ${GATEWAY}:18333 -events 0 -runtime 10m`,
456
+ "lock",
457
+ `volume.configure.replication -replication ${replication} -collectionPattern *`,
458
+ ...(replication === "000" ? [] : ["volume.fix.replication -apply"]),
459
+ "unlock",
460
+ ];
203
461
  return {
204
462
  apiVersion: "batch/v1",
205
463
  kind: "CronJob",
@@ -212,11 +470,13 @@ function lifecyclePass(config) {
212
470
  jobTemplate: {
213
471
  spec: {
214
472
  backoffLimit: 1,
215
- activeDeadlineSeconds: 900,
473
+ // Its copies' too, which a lost volume server's many take.
474
+ activeDeadlineSeconds: 2700,
216
475
  template: {
217
476
  metadata: { labels: stackLabels(component) },
218
477
  spec: {
219
478
  restartPolicy: "Never",
479
+ ...priority("component"),
220
480
  ...stackPodSpec(config),
221
481
  containers: [{
222
482
  name: "lifecycle",
@@ -226,7 +486,8 @@ function lifecyclePass(config) {
226
486
  "/bin/sh",
227
487
  "-c",
228
488
  // Waits out the moment before its NetworkPolicy applies, as a new pod.
229
- `until wget -q -O /dev/null http://${name}:9333/cluster/status; do sleep 2; done && printf 's3.lifecycle.run-shard -shards 0-15 -s3 ${name}:18333 -events 0 -runtime 10m\\n' | weed shell -master=${name}:9333 -filer=${name}:8888`,
489
+ `until wget -q -O /dev/null http://${MASTER}:9333/cluster/status; do sleep 2; done && ` +
490
+ `printf '${commands.map((command) => `${command}\\n`).join("")}' | weed shell -master=${masters(config)} -filer=${FILER}:8888`,
230
491
  ],
231
492
  securityContext: restrictedContainer(),
232
493
  resources: { requests: { cpu: "10m", memory: "32Mi" }, limits: { memory: "256Mi" } },
@@ -240,8 +501,208 @@ function lifecyclePass(config) {
240
501
  },
241
502
  };
242
503
  }
504
+ /**
505
+ * The filer store's backups: a snapshot of the whole store, every bucket, key, version and
506
+ * tag, and the log of changes lifecycle passes replay, as Valkey writes one for a replica,
507
+ * taken by the user that may do that alone, on the backups' schedule; written under a
508
+ * name of its own, then renamed, so a backup that fails leaves none behind but its
509
+ * predecessors, of which the last `keep` are kept. On a volume of their own, outside the
510
+ * store they copy, made, where the cluster has several nodes and another can take it, on
511
+ * another node than the filer store's (the volume is placed by the pod that first mounts
512
+ * it, and keeps its pods on its node from then on). `alasio restore-filer-store` puts one
513
+ * back (filerStoreRestore).
514
+ */
515
+ function filerStoreBackup(config) {
516
+ const { filerStore } = config.objectStore.bundled;
517
+ if (!filerStore.backups.enabled)
518
+ return [];
519
+ const claim = {
520
+ apiVersion: "v1",
521
+ kind: "PersistentVolumeClaim",
522
+ metadata: { name: BACKUPS, namespace: NAMESPACE, labels: labels(BACKUP_COMPONENT) },
523
+ spec: claimSpec(filerStore.backups.storage),
524
+ };
525
+ const save = {
526
+ name: "save",
527
+ image: imageReference(filerStore.image),
528
+ imagePullPolicy: "IfNotPresent",
529
+ command: [
530
+ "/bin/sh",
531
+ "-c",
532
+ script("set -eu", "stamp=$(date -u +%Y%m%dT%H%M%SZ)",
533
+ // What a backup that failed left behind, which no other is writing: they run one at a time.
534
+ "rm -f /backups/.filer-store-*.rdb",
535
+ // Waits out the moment before its NetworkPolicy applies, as a new pod.
536
+ `until valkey-cli -h ${FILER_STORE} --user ${FILER_STORE_USERS.backup.name} ping 2>/dev/null | grep -q PONG; do sleep 2; done`, `valkey-cli -h ${FILER_STORE} --user ${FILER_STORE_USERS.backup.name} --rdb "/backups/.filer-store-$stamp.rdb"`, 'test -s "/backups/.filer-store-$stamp.rdb"', 'mv "/backups/.filer-store-$stamp.rdb" "/backups/filer-store-$stamp.rdb"',
537
+ // All but the newest KEEP, which sort by when they were made.
538
+ "ls /backups | grep '^filer-store-.*\\.rdb$' | sort -r | tail -n +$((KEEP + 1)) | while read -r old; do rm \"/backups/$old\"; done"),
539
+ ],
540
+ env: [{ name: "KEEP", value: String(filerStore.backups.keep) }, { name: "VALKEYCLI_AUTH", valueFrom: valkeyPassword(FILER_STORE_USERS.backup.secret) }],
541
+ securityContext: { ...restrictedContainer(), readOnlyRootFilesystem: true },
542
+ resources: { requests: { cpu: "10m", memory: "64Mi" }, limits: { memory: "512Mi" } },
543
+ volumeMounts: [{ name: "backups", mountPath: "/backups" }],
544
+ };
545
+ const cronJob = {
546
+ apiVersion: "batch/v1",
547
+ kind: "CronJob",
548
+ metadata: { name: componentName(BACKUP_COMPONENT), namespace: NAMESPACE, labels: labels(BACKUP_COMPONENT) },
549
+ spec: {
550
+ schedule: filerStore.backups.schedule,
551
+ concurrencyPolicy: "Forbid",
552
+ successfulJobsHistoryLimit: 1,
553
+ failedJobsHistoryLimit: 3,
554
+ jobTemplate: {
555
+ spec: {
556
+ backoffLimit: 2,
557
+ activeDeadlineSeconds: 1800,
558
+ template: {
559
+ metadata: { labels: labels(BACKUP_COMPONENT) },
560
+ spec: {
561
+ restartPolicy: "Never",
562
+ ...priority("component"),
563
+ ...valkeyPod(config),
564
+ affinity: { podAntiAffinity: { preferredDuringSchedulingIgnoredDuringExecution: [awayFrom(COMPONENTS.filerStore)] } },
565
+ containers: [save],
566
+ volumes: [{ name: "backups", persistentVolumeClaim: { claimName: BACKUPS } }],
567
+ },
568
+ },
569
+ },
570
+ },
571
+ },
572
+ };
573
+ return [claim, cronJob];
574
+ }
575
+ /** A preference for another node than `component`'s pods'. */
576
+ const awayFrom = (component) => ({
577
+ weight: 100,
578
+ podAffinityTerm: { labelSelector: { matchLabels: selectorLabels(component) }, topologyKey: "kubernetes.io/hostname" },
579
+ });
580
+ /** The components of filerStoreRestore's pods: the one that serves the backup, and the one that loads it. */
581
+ export const RESTORE_COMPONENTS = { source: "seaweedfs-filer-store-restore-source", restore: "seaweedfs-filer-store-restore" };
582
+ /** The Jobs, and the source's Service, of filerStoreRestore, by their names. */
583
+ export const RESTORE_SOURCE = componentName(RESTORE_COMPONENTS.source);
584
+ export const RESTORE = componentName(RESTORE_COMPONENTS.restore);
585
+ export const BACKUPS_LISTING = componentName("seaweedfs-filer-store-backups-listing");
586
+ /** The filer store's volume, its StatefulSet's one pod's. */
587
+ export const FILER_STORE_CLAIM = `data-${FILER_STORE}-0`;
588
+ /** What filerStoreRestore's Job prints as its last line once it has loaded the backup: `restored <keys> keys`. */
589
+ export const RESTORED = /^restored (\d+) keys$/mu;
590
+ /**
591
+ * A Job that lists the filer store's backups, on their volume: what it prints, each
592
+ * backup's file's name, one a line.
593
+ */
594
+ export function backupsListing(config) {
595
+ return {
596
+ apiVersion: "batch/v1",
597
+ kind: "Job",
598
+ metadata: { name: BACKUPS_LISTING, namespace: NAMESPACE, labels: labels(BACKUP_COMPONENT) },
599
+ spec: {
600
+ backoffLimit: 0,
601
+ activeDeadlineSeconds: 600,
602
+ ttlSecondsAfterFinished: 3600,
603
+ template: {
604
+ metadata: { labels: labels(BACKUP_COMPONENT) },
605
+ spec: {
606
+ restartPolicy: "Never",
607
+ ...priority("component"),
608
+ ...valkeyPod(config),
609
+ containers: [{
610
+ name: "list",
611
+ image: imageReference(config.objectStore.bundled.filerStore.image),
612
+ imagePullPolicy: "IfNotPresent",
613
+ command: ["/bin/sh", "-c", "ls /backups | grep '^filer-store-.*\\.rdb$' || true"],
614
+ securityContext: { ...restrictedContainer(), readOnlyRootFilesystem: true },
615
+ resources: { requests: { cpu: "10m", memory: "16Mi" }, limits: { memory: "64Mi" } },
616
+ volumeMounts: [{ name: "backups", mountPath: "/backups", readOnly: true }],
617
+ }],
618
+ volumes: [{ name: "backups", persistentVolumeClaim: { claimName: BACKUPS, readOnly: true } }],
619
+ },
620
+ },
621
+ },
622
+ };
623
+ }
624
+ /**
625
+ * The filer store put back from its backup `file`, which `alasio restore-filer-store` runs
626
+ * with the filer store stopped, as two Jobs, since the backups' volume and the filer
627
+ * store's may be on different nodes: on the backups' node, a Valkey that loads the backup
628
+ * and serves it, behind a Service, to the other alone (./network-policies.ts), with the
629
+ * backup user's password; and on the filer store's, one that takes a snapshot of it, as the
630
+ * backups do, onto the filer store's volume beside what is there, loads it in a Valkey of
631
+ * its own, checks it holds as many keys as the backup, and only then deletes what was
632
+ * there and writes the append-only file the filer store loads as it starts, then prints
633
+ * how many keys it restored (RESTORED).
634
+ */
635
+ export function filerStoreRestore(config, file) {
636
+ const { filerStore } = config.objectStore.bundled;
637
+ const image = imageReference(filerStore.image);
638
+ const auth = { name: "VALKEYCLI_AUTH", valueFrom: valkeyPassword(FILER_STORE_USERS.backup.secret) };
639
+ const container = { image, imagePullPolicy: "IfNotPresent", securityContext: { ...restrictedContainer(), readOnlyRootFilesystem: true }, env: [auth] };
640
+ const job = (name, component, spec) => ({
641
+ apiVersion: "batch/v1",
642
+ kind: "Job",
643
+ metadata: { name, namespace: NAMESPACE, labels: labels(component) },
644
+ spec: {
645
+ backoffLimit: 0,
646
+ activeDeadlineSeconds: 3600,
647
+ template: { metadata: { labels: labels(component) }, spec: { restartPolicy: "Never", ...priority("component"), ...valkeyPod(config), ...spec } },
648
+ },
649
+ });
650
+ const service = {
651
+ apiVersion: "v1",
652
+ kind: "Service",
653
+ metadata: { name: RESTORE_SOURCE, namespace: NAMESPACE, labels: labels(RESTORE_COMPONENTS.source) },
654
+ spec: { selector: selectorLabels(RESTORE_COMPONENTS.source), ports: [{ name: "valkey", port: VALKEY_PORT }] },
655
+ };
656
+ const source = job(RESTORE_SOURCE, RESTORE_COMPONENTS.source, {
657
+ containers: [{
658
+ ...container,
659
+ name: "serve",
660
+ // Writes nothing: it neither snapshots nor appends.
661
+ command: ["valkey-server", "--port", String(VALKEY_PORT), "--dir", "/backups", "--dbfilename", file, "--appendonly", "no", "--save", "", "--requirepass", "$(VALKEYCLI_AUTH)"],
662
+ ports: [{ name: "valkey", containerPort: VALKEY_PORT }],
663
+ readinessProbe: { exec: { command: ["/bin/sh", "-c", "valkey-cli ping | grep -q PONG"] }, periodSeconds: 2 },
664
+ resources: { requests: { cpu: "10m", memory: "64Mi" }, limits: { memory: filerStore.resources.limits["memory"] ?? "512Mi" } },
665
+ volumeMounts: [{ name: "backups", mountPath: "/backups", readOnly: true }],
666
+ }],
667
+ volumes: [{ name: "backups", persistentVolumeClaim: { claimName: BACKUPS, readOnly: true } }],
668
+ });
669
+ const local = `valkey-cli -p ${VALKEY_PORT + 1}`;
670
+ const restore = job(RESTORE, RESTORE_COMPONENTS.restore, {
671
+ containers: [{
672
+ ...container,
673
+ name: "restore",
674
+ command: [
675
+ "/bin/sh",
676
+ "-c",
677
+ script("set -eu", `backup() { valkey-cli -h ${RESTORE_SOURCE} "$@"; }`,
678
+ // Waits for the backup to be loaded and served, and for its NetworkPolicy to apply, as a new pod.
679
+ "until backup ping 2>/dev/null | grep -q PONG; do sleep 2; done", "expected=$(backup dbsize)",
680
+ // The backup fetched and checked beside what the volume holds, which goes only once it is.
681
+ "rm -f /data/restore.rdb", "backup --rdb /data/restore.rdb", "test -s /data/restore.rdb", `valkey-server --port ${VALKEY_PORT + 1} --bind 127.0.0.1 --dir /data --dbfilename restore.rdb --appendonly no --save "" --requirepass "$VALKEYCLI_AUTH" &`, "server=$!",
682
+ // Each wait ends with the server, should it stop.
683
+ `until ${local} ping 2>/dev/null | grep -q PONG; do kill -0 "$server"; sleep 1; done`, `loaded=$(${local} dbsize)`, '[ "$loaded" = "$expected" ] || { echo "loaded $loaded keys, and the backup has $expected" >&2; exit 1; }', "rm -rf /data/appendonlydir /data/dump.rdb", `${local} config set appendonly yes >/dev/null`,
684
+ // The rewrite that writes the append-only file from what is loaded, done, and well.
685
+ `until persistence=$(${local} info persistence) && echo "$persistence" | grep -q '^aof_enabled:1' && echo "$persistence" | grep -q '^aof_rewrite_in_progress:0'; do kill -0 "$server"; sleep 1; done`, `${local} info persistence | grep -q '^aof_last_bgrewrite_status:ok' && [ -n "$(ls /data/appendonlydir)" ] || { echo "the append-only file could not be written" >&2; exit 1; }`, `${local} shutdown`, "wait", "rm /data/restore.rdb", 'echo "restored $loaded keys"'),
686
+ ],
687
+ resources: { requests: { cpu: "10m", memory: "64Mi" }, limits: { memory: filerStore.resources.limits["memory"] ?? "512Mi" } },
688
+ volumeMounts: [{ name: "data", mountPath: "/data" }],
689
+ }],
690
+ volumes: [{ name: "data", persistentVolumeClaim: { claimName: FILER_STORE_CLAIM } }],
691
+ });
692
+ return [service, source, restore];
693
+ }
243
694
  /** The bundled object store's objects, when it runs, which is beside Neon. */
244
695
  export function objectStoreObjects(config) {
245
- return config.neon.enabled && config.objectStore.bundled.enabled ? [...server(config), lifecyclePass(config)] : [];
696
+ if (!config.neon.enabled || !config.objectStore.bundled.enabled)
697
+ return [];
698
+ return [
699
+ ...masterObjects(config),
700
+ ...volumeServerObjects(config),
701
+ ...filerStoreObjects(config),
702
+ ...filerObjects(config),
703
+ ...gatewayObjects(config),
704
+ lifecyclePass(config),
705
+ ...filerStoreBackup(config),
706
+ ];
246
707
  }
247
708
  //# sourceMappingURL=object-store.js.map