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.
- package/dist/cli/src/branches.js +53 -22
- package/dist/cli/src/branches.js.map +1 -1
- package/dist/cli/src/capacity.js +46 -0
- package/dist/cli/src/capacity.js.map +1 -0
- package/dist/cli/src/cluster/docker-engine.js +6 -4
- package/dist/cli/src/cluster/docker-engine.js.map +1 -1
- package/dist/cli/src/cluster/docker.js +49 -10
- package/dist/cli/src/cluster/docker.js.map +1 -1
- package/dist/cli/src/cluster/host.js +45 -19
- package/dist/cli/src/cluster/host.js.map +1 -1
- package/dist/cli/src/cluster/k3s.js +109 -9
- package/dist/cli/src/cluster/k3s.js.map +1 -1
- package/dist/cli/src/cluster/node.js +94 -50
- package/dist/cli/src/cluster/node.js.map +1 -1
- package/dist/cli/src/cluster/tar.js +43 -1
- package/dist/cli/src/cluster/tar.js.map +1 -1
- package/dist/cli/src/commands/branch.js +9 -4
- package/dist/cli/src/commands/branch.js.map +1 -1
- package/dist/cli/src/commands/restore-filer-store.js +247 -0
- package/dist/cli/src/commands/restore-filer-store.js.map +1 -0
- package/dist/cli/src/commands/status.js +141 -12
- package/dist/cli/src/commands/status.js.map +1 -1
- package/dist/cli/src/commands/uninstall.js +1 -1
- package/dist/cli/src/commands/uninstall.js.map +1 -1
- package/dist/cli/src/commands/up.js +42 -4
- package/dist/cli/src/commands/up.js.map +1 -1
- package/dist/cli/src/commands/upgrade.js +22 -6
- package/dist/cli/src/commands/upgrade.js.map +1 -1
- package/dist/cli/src/commands.js +2 -1
- package/dist/cli/src/commands.js.map +1 -1
- package/dist/cli/src/config.js +13 -4
- package/dist/cli/src/config.js.map +1 -1
- package/dist/cli/src/cpu-baseline.js +97 -0
- package/dist/cli/src/cpu-baseline.js.map +1 -0
- package/dist/cli/src/install.js +44 -11
- package/dist/cli/src/install.js.map +1 -1
- package/dist/cli/src/kept-releases.js +59 -0
- package/dist/cli/src/kept-releases.js.map +1 -0
- package/dist/cli/src/kube/api.js +13 -5
- package/dist/cli/src/kube/api.js.map +1 -1
- package/dist/cli/src/kube/apply.js +116 -13
- package/dist/cli/src/kube/apply.js.map +1 -1
- package/dist/cli/src/kube/autoscalers.js +48 -0
- package/dist/cli/src/kube/autoscalers.js.map +1 -0
- package/dist/cli/src/kube/object-store-scale.js +116 -0
- package/dist/cli/src/kube/object-store-scale.js.map +1 -0
- package/dist/cli/src/kube/rollout.js +23 -0
- package/dist/cli/src/kube/rollout.js.map +1 -1
- package/dist/cli/src/manifests/admission.js +416 -0
- package/dist/cli/src/manifests/admission.js.map +1 -0
- package/dist/cli/src/manifests/agent-sandbox.js +6 -3
- package/dist/cli/src/manifests/agent-sandbox.js.map +1 -1
- package/dist/cli/src/manifests/alasio.js +165 -25
- package/dist/cli/src/manifests/alasio.js.map +1 -1
- package/dist/cli/src/manifests/branch.js +2 -1
- package/dist/cli/src/manifests/branch.js.map +1 -1
- package/dist/cli/src/manifests/cloudeventsources.eventing.keda.sh.json +219 -0
- package/dist/cli/src/manifests/clustercloudeventsources.eventing.keda.sh.json +218 -0
- package/dist/cli/src/manifests/clustertriggerauthentications.keda.sh.json +997 -0
- package/dist/cli/src/manifests/collector.js +92 -45
- package/dist/cli/src/manifests/collector.js.map +1 -1
- package/dist/cli/src/manifests/common.js +34 -8
- package/dist/cli/src/manifests/common.js.map +1 -1
- package/dist/cli/src/manifests/config.js +273 -35
- package/dist/cli/src/manifests/config.js.map +1 -1
- package/dist/cli/src/manifests/controllers.js +39 -0
- package/dist/cli/src/manifests/controllers.js.map +1 -0
- package/dist/cli/src/manifests/disruption.js +47 -0
- package/dist/cli/src/manifests/disruption.js.map +1 -0
- package/dist/cli/src/manifests/grafana.js +66 -10
- package/dist/cli/src/manifests/grafana.js.map +1 -1
- package/dist/cli/src/manifests/headroom.js +40 -0
- package/dist/cli/src/manifests/headroom.js.map +1 -0
- package/dist/cli/src/manifests/index.js +20 -6
- package/dist/cli/src/manifests/index.js.map +1 -1
- package/dist/cli/src/manifests/juicefs-csi.js +9 -6
- package/dist/cli/src/manifests/juicefs-csi.js.map +1 -1
- package/dist/cli/src/manifests/keda.js +271 -0
- package/dist/cli/src/manifests/keda.js.map +1 -0
- package/dist/cli/src/manifests/lake.js +13 -5
- package/dist/cli/src/manifests/lake.js.map +1 -1
- package/dist/cli/src/manifests/neon.js +196 -51
- package/dist/cli/src/manifests/neon.js.map +1 -1
- package/dist/cli/src/manifests/network-policies.js +177 -28
- package/dist/cli/src/manifests/network-policies.js.map +1 -1
- package/dist/cli/src/manifests/node-agent.js +107 -0
- package/dist/cli/src/manifests/node-agent.js.map +1 -0
- package/dist/cli/src/manifests/object-store.js +543 -82
- package/dist/cli/src/manifests/object-store.js.map +1 -1
- package/dist/cli/src/manifests/priorities.js +31 -0
- package/dist/cli/src/manifests/priorities.js.map +1 -0
- package/dist/cli/src/manifests/scaledjobs.keda.sh.json +7507 -0
- package/dist/cli/src/manifests/scaledobjects.keda.sh.json +564 -0
- package/dist/cli/src/manifests/scalers.js +24 -0
- package/dist/cli/src/manifests/scalers.js.map +1 -0
- package/dist/cli/src/manifests/sizing.js +40 -0
- package/dist/cli/src/manifests/sizing.js.map +1 -0
- package/dist/cli/src/manifests/triggerauthentications.keda.sh.json +992 -0
- package/dist/cli/src/manifests/valkey.js +63 -35
- package/dist/cli/src/manifests/valkey.js.map +1 -1
- package/dist/cli/src/manifests/verticalpodautoscalercheckpoints.autoscaling.k8s.io.json +250 -0
- package/dist/cli/src/manifests/verticalpodautoscalers.autoscaling.k8s.io.json +837 -0
- package/dist/cli/src/manifests/vpa.js +220 -0
- package/dist/cli/src/manifests/vpa.js.map +1 -0
- package/dist/cli/src/manifests/workspace-storage.js +12 -2
- package/dist/cli/src/manifests/workspace-storage.js.map +1 -1
- package/dist/cli/src/object-store.js +57 -0
- package/dist/cli/src/object-store.js.map +1 -0
- package/dist/cli/src/release.js +6 -6
- package/dist/cli/src/target.js +1 -1
- package/dist/cli/src/target.js.map +1 -1
- package/dist/cluster/node/pins.json +6 -2
- package/dist/neon/control/branches.js +31 -12
- package/dist/neon/control/branches.js.map +1 -1
- package/dist/neon/control/endpoints.js +108 -0
- package/dist/neon/control/endpoints.js.map +1 -0
- package/dist/neon/control/scram.js +13 -1
- package/dist/neon/control/scram.js.map +1 -1
- package/dist/neon/control/secrets.js +52 -8
- package/dist/neon/control/secrets.js.map +1 -1
- package/dist/src/kube/busy-label.js +13 -0
- package/dist/src/kube/busy-label.js.map +1 -0
- package/dist/src/kube/config.js +30 -4
- package/dist/src/kube/config.js.map +1 -1
- package/dist/src/kube/sandbox-shape.js +98 -0
- package/dist/src/kube/sandbox-shape.js.map +1 -0
- package/dist/src/node-agent/names.js +74 -0
- package/dist/src/node-agent/names.js.map +1 -0
- package/dist/src/sandbox/names.js +44 -0
- package/dist/src/sandbox/names.js.map +1 -0
- package/package.json +2 -2
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/** The
|
|
17
|
-
const
|
|
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
|
|
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
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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(
|
|
82
|
-
spec: { selector: selectorLabels(
|
|
279
|
+
metadata: { name: FILER, namespace: NAMESPACE, labels: stackLabels(COMPONENTS.filer) },
|
|
280
|
+
spec: { selector: selectorLabels(COMPONENTS.filer), ports: PORTS.filer },
|
|
83
281
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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:
|
|
91
|
-
spec: {
|
|
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
|
|
329
|
+
const deployment = {
|
|
94
330
|
apiVersion: "apps/v1",
|
|
95
|
-
kind: "
|
|
96
|
-
metadata: { name, namespace: NAMESPACE, labels: stackLabels(
|
|
331
|
+
kind: "Deployment",
|
|
332
|
+
metadata: { name: GATEWAY, namespace: NAMESPACE, labels: stackLabels(COMPONENTS.gateway) },
|
|
97
333
|
spec: {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
selector: { matchLabels: selectorLabels("seaweedfs") },
|
|
334
|
+
...replicasOf(scaling),
|
|
335
|
+
selector: { matchLabels: selectorLabels(COMPONENTS.gateway) },
|
|
101
336
|
template: {
|
|
102
|
-
metadata: { labels: stackLabels(
|
|
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: "
|
|
345
|
+
name: "s3",
|
|
109
346
|
image,
|
|
110
347
|
imagePullPolicy: "IfNotPresent",
|
|
111
348
|
args: [
|
|
112
|
-
"
|
|
113
|
-
`-
|
|
349
|
+
"s3",
|
|
350
|
+
`-filer=${podAddresses(FILER, FILER_PEERS, bundled.filers.scaling.min, 8888)}`,
|
|
114
351
|
"-ip.bind=0.0.0.0",
|
|
115
|
-
"-
|
|
116
|
-
|
|
117
|
-
"-
|
|
118
|
-
|
|
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: "
|
|
361
|
+
resources: bundled.gateways.resources,
|
|
362
|
+
volumeMounts: [{ name: "identities", mountPath: "/etc/seaweedfs", readOnly: true }],
|
|
144
363
|
},
|
|
145
|
-
// Makes the buckets, idempotently,
|
|
146
|
-
//
|
|
147
|
-
// once
|
|
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",
|
|
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
|
|
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",
|
|
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 [
|
|
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
|
|
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
|
|
202
|
-
const
|
|
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
|
-
|
|
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://${
|
|
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
|
-
|
|
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
|