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