Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45f44023a8 | |||
| 173a54a83c | |||
| 41c943a6b1 | |||
| 84c1e189c4 | |||
| dc8355d0f3 | |||
| 1c12ca4caf | |||
| 582b168b6a | |||
| c821d4974a | |||
| eb1b621a5a | |||
| 0c32294485 | |||
| 21500545bb | |||
| a525d0e36a | |||
| 29e869ac88 | |||
| cd641a9ed2 | |||
| 68876f0b08 | |||
| ba1f30d393 | |||
| 9018ed9b97 | |||
| 13ded6dd35 | |||
| 4d96719d96 | |||
| 77ef3c85ea | |||
| 79c7126b67 | |||
| 77b6233496 | |||
| 547fe30a05 | |||
| bff5068efc | |||
| 60eee993b4 | |||
| d30bb2acb1 | |||
| 730c613807 | |||
| 9e356347c6 | |||
| 1d3c0511b1 | |||
| f50b342c00 | |||
| cf22e7b71d | |||
| dc8d93698b | |||
| 006e78ccea | |||
| 276e55ae8b | |||
| ab6d9633ac | |||
| cf2f058f60 | |||
| 02677af546 | |||
| fced78c283 | |||
| d211c1e291 | |||
| 318ef40c4e | |||
| c9c50b741e | |||
| a0a2c1db88 | |||
| c685a2cbaf | |||
| 04d549acc7 | |||
| a9cd3f3426 | |||
| 8f7e92b6a7 | |||
| 969f42a970 | |||
| c2d54b4136 | |||
| 424d4f8d4d | |||
| 2f7a649870 | |||
| c4836916b0 | |||
| bf5290036f | |||
| 4efc8bac07 | |||
| f3dcc39903 | |||
| b830bdd1dd | |||
| 161b185464 | |||
| 8f0d10b0b1 | |||
| 43e02920c2 | |||
| dcc2fe4ac5 | |||
| 6bac369ee2 | |||
| 96d303b05e | |||
| 3a1dce59f7 | |||
| e3a5ec6ef6 | |||
| 4d124e1c76 | |||
| 6fd2cf7966 | |||
| 6aecd9718f | |||
| d769a7be5d | |||
| 0412013229 | |||
| 511cf0c6ec | |||
| 090dbb412a | |||
| 95693d45b2 | |||
| ca296477f3 | |||
| ca3b4a050d | |||
| a057ab23ea | |||
| 12367d307b | |||
| 675c1c156d | |||
| eac3a60050 | |||
| 58bc65b9a8 | |||
| a5580c99fe | |||
| 8083bb4a0f | |||
| 00cdefa6b3 | |||
| 4dee3e6e04 | |||
| 5333285b50 | |||
| ef913843f7 | |||
| ac851d6dee | |||
| eac2aa6fe4 | |||
| 1e0201ada2 | |||
| 1fe932d07f | |||
| 82297371bf | |||
| 7e5bb51287 | |||
| 00a5c3d8a2 | |||
| 174f4f01a8 | |||
| 1aac7b4875 | |||
| b43c58cbe5 | |||
| 9481ac428e | |||
| 1c29d04cc5 | |||
| b48a8eaa1f | |||
| 42fd8583bd | |||
| 2e7a6fccba | |||
| 09041035d5 | |||
| ae64ecf10c | |||
| c8ac4a2105 | |||
| 280c1303fa | |||
| 0f5b3878ca | |||
| d863247f9f | |||
| 6c740ff05c | |||
| 95d9905524 | |||
| 53fe77860b | |||
| 6d5e971974 | |||
| 4d8407dc0f | |||
| 236af3a958 | |||
| 4b1fdbef55 | |||
| 0f1b488be0 |
@@ -12,32 +12,32 @@ when:
|
||||
|
||||
steps:
|
||||
- name: check formatting
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-shell --attr devShell --run "cargo fmt -- --check"
|
||||
- nix-build -j4 --attr flakePackages.fmt
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build -j4 --attr flakePackages.dev
|
||||
|
||||
- name: unit + func tests (lmdb)
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build -j4 --attr flakePackages.tests-lmdb
|
||||
|
||||
- name: unit + func tests (sqlite)
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build -j4 --attr flakePackages.tests-sqlite
|
||||
|
||||
- name: unit + func tests (fjall)
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build -j4 --attr flakePackages.tests-fjall
|
||||
|
||||
- name: integration tests
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build -j4 --attr flakePackages.dev
|
||||
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
@@ -11,7 +11,7 @@ depends_on:
|
||||
|
||||
steps:
|
||||
- name: refresh-index
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: garagehq_aws_access_key_id
|
||||
@@ -22,7 +22,7 @@ steps:
|
||||
- nix-shell --attr ci --run "refresh_index"
|
||||
|
||||
- name: multiarch-docker
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
environment:
|
||||
DOCKER_AUTH:
|
||||
from_secret: docker_auth
|
||||
|
||||
@@ -19,17 +19,17 @@ matrix:
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build --attr releasePackages.${ARCH} --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
|
||||
|
||||
- name: check is static binary
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-shell --attr ci --run "./script/not-dynamic.sh result/bin/garage"
|
||||
|
||||
- name: integration tests
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
when:
|
||||
@@ -39,7 +39,7 @@ steps:
|
||||
ARCH: i386
|
||||
|
||||
- name: upgrade tests from v1.0.0
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-shell --attr ci --run "./script/test-upgrade.sh v1.0.0 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
|
||||
when:
|
||||
@@ -47,7 +47,7 @@ steps:
|
||||
ARCH: amd64
|
||||
|
||||
- name: upgrade tests from v0.8.4
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-shell --attr ci --run "./script/test-upgrade.sh v0.8.4 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
|
||||
when:
|
||||
@@ -55,7 +55,7 @@ steps:
|
||||
ARCH: amd64
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
environment:
|
||||
TARGET: "${TARGET}"
|
||||
AWS_ACCESS_KEY_ID:
|
||||
@@ -66,7 +66,7 @@ steps:
|
||||
- nix-shell --attr ci --run "to_s3"
|
||||
|
||||
- name: docker build and publish
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
environment:
|
||||
DOCKER_PLATFORM: "linux/${ARCH}"
|
||||
CONTAINER_NAME: "dxflrs/${ARCH}_garage"
|
||||
|
||||
@@ -24,18 +24,18 @@ default-members = ["src/garage"]
|
||||
|
||||
# Internal Garage crates
|
||||
format_table = { version = "0.1.1", path = "src/format-table" }
|
||||
garage_api_common = { version = "2.1.0", path = "src/api/common" }
|
||||
garage_api_admin = { version = "2.1.0", path = "src/api/admin" }
|
||||
garage_api_s3 = { version = "2.1.0", path = "src/api/s3" }
|
||||
garage_api_k2v = { version = "2.1.0", path = "src/api/k2v" }
|
||||
garage_block = { version = "2.1.0", path = "src/block" }
|
||||
garage_db = { version = "2.1.0", path = "src/db", default-features = false }
|
||||
garage_model = { version = "2.1.0", path = "src/model", default-features = false }
|
||||
garage_net = { version = "2.1.0", path = "src/net" }
|
||||
garage_rpc = { version = "2.1.0", path = "src/rpc" }
|
||||
garage_table = { version = "2.1.0", path = "src/table" }
|
||||
garage_util = { version = "2.1.0", path = "src/util" }
|
||||
garage_web = { version = "2.1.0", path = "src/web" }
|
||||
garage_api_common = { version = "2.2.0", path = "src/api/common" }
|
||||
garage_api_admin = { version = "2.2.0", path = "src/api/admin" }
|
||||
garage_api_s3 = { version = "2.2.0", path = "src/api/s3" }
|
||||
garage_api_k2v = { version = "2.2.0", path = "src/api/k2v" }
|
||||
garage_block = { version = "2.2.0", path = "src/block" }
|
||||
garage_db = { version = "2.2.0", path = "src/db", default-features = false }
|
||||
garage_model = { version = "2.2.0", path = "src/model", default-features = false }
|
||||
garage_net = { version = "2.2.0", path = "src/net" }
|
||||
garage_rpc = { version = "2.2.0", path = "src/rpc" }
|
||||
garage_table = { version = "2.2.0", path = "src/table" }
|
||||
garage_util = { version = "2.2.0", path = "src/util" }
|
||||
garage_web = { version = "2.2.0", path = "src/web" }
|
||||
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
|
||||
|
||||
# External crates from crates.io
|
||||
@@ -49,9 +49,8 @@ bytes = "1.0"
|
||||
bytesize = "1.1"
|
||||
cfg-if = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
crc-fast = "1.1"
|
||||
crc-fast = "1.6"
|
||||
crypto-common = "0.1"
|
||||
err-derive = "0.3"
|
||||
gethostname = "0.4"
|
||||
git-version = "0.3.4"
|
||||
hex = "0.4"
|
||||
@@ -138,7 +137,7 @@ prometheus = "0.13"
|
||||
aws-sigv4 = { version = "1.1", default-features = false }
|
||||
hyper-rustls = { version = "0.26", default-features = false, features = ["http1", "http2", "ring", "rustls-native-certs"] }
|
||||
log = "0.4"
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0"
|
||||
|
||||
# ---- used only as build / dev dependencies ----
|
||||
assert-json-diff = "2.0"
|
||||
@@ -155,5 +154,5 @@ lto = "off"
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = "s"
|
||||
opt-level = 3
|
||||
strip = true
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"name": "AGPL-3.0",
|
||||
"identifier": "AGPL-3.0"
|
||||
},
|
||||
"version": "v2.1.0"
|
||||
"version": "v2.2.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@@ -103,7 +103,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AddBucketAliasRequest"
|
||||
"$ref": "#/components/schemas/BucketAliasEnum"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1409,7 +1409,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RemoveBucketAliasRequest"
|
||||
"$ref": "#/components/schemas/BucketAliasEnum"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1722,24 +1722,6 @@
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"AddBucketAliasRequest": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/BucketAliasEnum"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bucketId"
|
||||
],
|
||||
"properties": {
|
||||
"bucketId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"AddBucketAliasResponse": {
|
||||
"$ref": "#/components/schemas/GetBucketInfoResponse"
|
||||
},
|
||||
@@ -1957,9 +1939,13 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bucketId",
|
||||
"globalAlias"
|
||||
],
|
||||
"properties": {
|
||||
"bucketId": {
|
||||
"type": "string"
|
||||
},
|
||||
"globalAlias": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1968,6 +1954,7 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bucketId",
|
||||
"localAlias",
|
||||
"accessKeyId"
|
||||
],
|
||||
@@ -1975,6 +1962,9 @@
|
||||
"accessKeyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"bucketId": {
|
||||
"type": "string"
|
||||
},
|
||||
"localAlias": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -3912,6 +3902,49 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"NodeRoleChangeRequest": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"remove"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the node for which this change applies"
|
||||
},
|
||||
"remove": {
|
||||
"type": "boolean",
|
||||
"description": "Set `remove` to `true` to remove the node from the layout"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/NodeRoleUpdate"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NodeRoleUpdate": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NodeAssignedRole"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the node for which this change applies"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"NodeUpdateTrackers": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -3973,24 +4006,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"RemoveBucketAliasRequest": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/BucketAliasEnum"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bucketId"
|
||||
],
|
||||
"properties": {
|
||||
"bucketId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"RemoveBucketAliasResponse": {
|
||||
"$ref": "#/components/schemas/GetBucketInfoResponse"
|
||||
},
|
||||
@@ -4180,7 +4195,7 @@
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NodeRoleChange"
|
||||
"$ref": "#/components/schemas/NodeRoleChangeRequest"
|
||||
},
|
||||
"description": "New node roles to assign or remove in the cluster layout"
|
||||
}
|
||||
@@ -4338,43 +4353,39 @@
|
||||
"WorkerStateResp": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"busy"
|
||||
]
|
||||
},
|
||||
"$ref": "#/components/schemas/Yolo"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/WorkerStateRespStrs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Yolo": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"throttled"
|
||||
],
|
||||
"properties": {
|
||||
"throttled": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"throttled"
|
||||
"durationSecs"
|
||||
],
|
||||
"properties": {
|
||||
"throttled": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"durationSecs"
|
||||
],
|
||||
"properties": {
|
||||
"durationSecs": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
}
|
||||
"durationSecs": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"idle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"done"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"WorkerStateRespStrs": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"busy",
|
||||
"idle",
|
||||
"done"
|
||||
]
|
||||
},
|
||||
"ZoneRedundancy": {
|
||||
|
||||
@@ -12,8 +12,9 @@ In this section, we cover the following web applications:
|
||||
| [Mastodon](#mastodon) | ✅ | Natively supported |
|
||||
| [Matrix](#matrix) | ✅ | Tested with `synapse-s3-storage-provider` |
|
||||
| [ejabberd](#ejabberd) | ✅ | `mod_s3_upload` |
|
||||
| [Pixelfed](#pixelfed) | ✅ | Natively supported |
|
||||
| [Pleroma](#pleroma) | ❓ | Not yet tested |
|
||||
| [Ente](#ente) | ✅ | Natively supported |
|
||||
| [Pixelfed](#pixelfed) | ❓ | Natively supported |
|
||||
| [Pleroma](#pleroma) | ✅ | Natively supported |
|
||||
| [Lemmy](#lemmy) | ✅ | Supported with pict-rs |
|
||||
| [Funkwhale](#funkwhale) | ❓ | Not yet tested |
|
||||
| [Misskey](#misskey) | ❓ | Not yet tested |
|
||||
@@ -567,13 +568,186 @@ The module can then be configured with:
|
||||
Other configuration options can be found in the
|
||||
[configuration YAML file](https://github.com/processone/ejabberd-contrib/blob/master/mod_s3_upload/conf/mod_s3_upload.yml).
|
||||
|
||||
|
||||
## Ente
|
||||
|
||||
Ente is an alternative for Google Photos and Apple Photos. It [can be selfhosted](https://help.ente.io/self-hosting/) and is working fine with Garage as of May 2024.
|
||||
As a first step we need to create a bucket and a key for Ente:
|
||||
|
||||
```bash
|
||||
garage bucket create ente
|
||||
garage key create ente-key
|
||||
# For the CORS setup to work, the key needs to be --owner as well, at least temporarily.
|
||||
garage bucket allow ente --read --write --owner --key ente-key
|
||||
```
|
||||
|
||||
We also need to setup some CORS rules to allow the Ente frontend to access the bucket:
|
||||
|
||||
```bash
|
||||
export CORS='{"CORSRules":[{"AllowedHeaders":["*"],"AllowedMethods":["GET", "PUT", "POST", "DELETE"],"AllowedOrigins":["*"], "ExposeHeaders":["ETag"]}]}'
|
||||
aws s3api put-bucket-cors --bucket ente --cors-configuration $CORS
|
||||
```
|
||||
|
||||
Now we need to configure ente-server to use our bucket. This is explained [in the Ente S3 documentation](https://help.ente.io/self-hosting/guides/external-s3).
|
||||
Prepare a configuration file for ente's backend as `museum.yaml`:
|
||||
|
||||
```yaml
|
||||
credentials-file: /credentials.yaml
|
||||
apps:
|
||||
public-albums: https://albums.example.tld # If you want to use the share album feature
|
||||
internal:
|
||||
hardcoded-ott:
|
||||
local-domain-suffix: "@example.com" # Your domain
|
||||
local-domain-value: 123456 # Custom One-Time Password since we are not sending mail by default
|
||||
key:
|
||||
# WARNING -- You MUST CHANGE the values below
|
||||
# Someone has made an image that can do it for you : https://github.com/EdyTheCow/ente-selfhost/blob/main/images/ente-server-tools/Dockerfile
|
||||
# Simply build it yourself or run docker run --rm ghcr.io/edythecow/ente-server-tools go run tools/gen-random-keys/main.go
|
||||
encryption: yvmG/RnzKrbCb9L3mgsmoxXr9H7i2Z4qlbT0mL3ln4w= # CHANGE THIS VALUE
|
||||
hash: KXYiG07wC7GIgvCSdg+WmyWdXDAn6XKYJtp/wkEU7x573+byBRAYtpTP0wwvi8i/4l37uicX1dVTUzwH3sLZyw== # CHANGE THIS VALUE
|
||||
jwt:
|
||||
secret: i2DecQmfGreG6q1vBj5tCokhlN41gcfS2cjOs9Po-u8= # CHANGE THIS VALUE
|
||||
```
|
||||
|
||||
The full configuration file can be found [here](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml)
|
||||
Then prepare a credentials file as `credentials.yaml`
|
||||
|
||||
```yaml
|
||||
db:
|
||||
host: postgres
|
||||
port: 5432
|
||||
name: <ente_db_name>
|
||||
user: <pguser>
|
||||
password: <pgpass>
|
||||
|
||||
s3:
|
||||
# Override the primary and secondary hot storage. The commented out values
|
||||
# are the defaults.
|
||||
#
|
||||
hot_storage:
|
||||
primary: b2-eu-cen
|
||||
# secondary: wasabi-eu-central-2-v3
|
||||
|
||||
# If true, enable some workarounds to allow us to use a local minio instance
|
||||
# for object storage.
|
||||
#
|
||||
# 1. Disable SSL.
|
||||
# 2. Use "path" style S3 URLs (see `use_path_style_urls` below).
|
||||
# 3. Directly download the file during replication instead of going via the
|
||||
# Cloudflare worker.
|
||||
# 4. Do not specify storage classes when uploading objects (since minio does
|
||||
# not support them, specifically it doesn't support GLACIER).
|
||||
are_local_buckets: true
|
||||
|
||||
# To use "path" style S3 URLs instead of DNS-based bucket access
|
||||
# default to true if you set "are_local_buckets: true"
|
||||
# use_path_style_urls: true
|
||||
|
||||
b2-eu-cen: # Don't change this key, it is hardcoded
|
||||
key: <keyID>
|
||||
secret: <keySecret>
|
||||
endpoint: garage:3900 # publically accessible endpoint of your garage instance
|
||||
region: garage
|
||||
bucket: <yourbucketName>
|
||||
use_path_style: true
|
||||
# you can specify secondary locations, names are hardcoded as well
|
||||
# wasabi-eu-central-2-v3:
|
||||
# scw-eu-fr-v3:
|
||||
|
||||
# and you can also specify a bucket to be used for embeddings, preview etc..
|
||||
# default to the first bucket
|
||||
# derived-storage: wasabi-eu-central-2-derived
|
||||
```
|
||||
|
||||
Finally you can run it with Docker :
|
||||
|
||||
```bash
|
||||
docker run -d --name ente-server --restart unless-stopped -v /path/to/museum.yaml:/museum.yaml -v /path/to/credentials.yaml:/credentials.yaml -p 8080:8080 ghcr.io/ente-io/ente-server
|
||||
```
|
||||
|
||||
For more information on deployment you can check the [ente documentation](https://help.ente.io/self-hosting/)
|
||||
|
||||
## Pixelfed
|
||||
|
||||
[Pixelfed Technical Documentation > Configuration](https://docs.pixelfed.org/technical-documentation/env.html#filesystem)
|
||||
|
||||
## Pleroma
|
||||
|
||||
[Pleroma Documentation > Pleroma.Uploaders.S3](https://docs-develop.pleroma.social/backend/configuration/cheatsheet/#pleromauploaderss3)
|
||||
### Creating your bucket
|
||||
|
||||
This is the usual Garage setup:
|
||||
|
||||
```bash
|
||||
garage key new --name pleroma-key
|
||||
garage bucket create pleroma
|
||||
garage bucket allow pleroma --read --write --owner --key pleroma-key
|
||||
```
|
||||
|
||||
We also need to expose these buckets publicly to serve their content to users:
|
||||
|
||||
```bash
|
||||
garage bucket website --allow pleroma
|
||||
```
|
||||
|
||||
Note the Key ID and Secret Key.
|
||||
|
||||
### Configure Pleroma
|
||||
|
||||
Update your Pleroma configuration like that in `/etc/pleroma/config.exs`.
|
||||
|
||||
```
|
||||
config :pleroma, Pleroma.Upload,
|
||||
uploader: Pleroma.Uploaders.S3,
|
||||
base_url: "https://pleroma.garage.example.tld"
|
||||
|
||||
config :ex_aws, :s3,
|
||||
access_key_id: "GW...",
|
||||
secret_access_key: "XXX",
|
||||
region: "garage",
|
||||
host: "api.garage.example.tld"
|
||||
```
|
||||
|
||||
And restart Pleroma.
|
||||
|
||||
You can found more information in [Pleroma Documentation > Pleroma.Uploaders.S3](https://docs-develop.pleroma.social/backend/configuration/cheatsheet/#pleromauploaderss3)
|
||||
|
||||
### Migrating your data
|
||||
|
||||
Pleroma have an internal migration tool that can encounter some fatal error
|
||||
|
||||
```
|
||||
** (EXIT from #PID<0.98.0>) an exception was raised:
|
||||
** (File.Error) could not stream "/var/lib/pleroma/uploads/09/f8": illegal operation on a directory
|
||||
(elixir 1.17.3) lib/file/stream.ex:100: anonymous fn/3 in Enumerable.File.Stream.reduce/3
|
||||
(elixir 1.17.3) lib/stream.ex:1675: anonymous fn/5 in Stream.resource/3
|
||||
(elixir 1.17.3) lib/stream.ex:1891: Enumerable.Stream.do_each/4
|
||||
(elixir 1.17.3) lib/task/supervised.ex:370: Task.Supervised.stream_reduce/7
|
||||
(elixir 1.17.3) lib/enum.ex:4423: Enum.map/2
|
||||
(ex_aws_s3 2.5.8) lib/ex_aws/s3/upload.ex:141: ExAws.Operation.ExAws.S3.Upload.perform/2
|
||||
(pleroma 2.10.0) lib/pleroma/uploaders/s3.ex:60: Pleroma.Uploaders.S3.put_file/1
|
||||
(pleroma 2.10.0) lib/pleroma/uploaders/uploader.ex:49: Pleroma.Uploaders.Uploader.put_file/2
|
||||
```
|
||||
|
||||
So, use [your best tool](https://garagehq.deuxfleurs.fr/documentation/connect/cli/) to sync `/var/lib/pleroma/uploads/` in your S3.
|
||||
|
||||
Then, to avoid some non existant problem (just in case of), run this command
|
||||
|
||||
```bash
|
||||
while true
|
||||
do
|
||||
rm -vr $(./bin/pleroma_ctl uploads migrate_local S3 2>&1 | grep "could not stream" | awk -F '"' '{print $2}')
|
||||
sleep 5
|
||||
done
|
||||
```
|
||||
|
||||
If you have many files, stop this command sometime and the command bellow (interactive) to delete local
|
||||
file after upload. Then restart the loop.
|
||||
|
||||
```bash
|
||||
./bin/pleroma_ctl uploads migrate_local S3 --delete
|
||||
```
|
||||
|
||||
And *voilà*
|
||||
|
||||
## Lemmy
|
||||
|
||||
|
||||
@@ -207,3 +207,13 @@ $ plakar at @garageS3 ls
|
||||
```
|
||||
|
||||
More information in Plakar documentation: https://www.plakar.io/docs/main/quickstart/
|
||||
|
||||
## Synology HyperBackup
|
||||
|
||||
HyperBackup can be configured to upload backups to garage using a custom S3 destination. However, the HyperBackup client hardcodes the `us-east-1` region that is a critical input to the v4 signature process. If garage is not set to `us-east-1`, HyperBackup will recognize available buckets, but fail during the final setup stage.
|
||||
|
||||
In garage.toml:
|
||||
```toml
|
||||
[s3_api]
|
||||
s3_region = "us-east-1"
|
||||
```
|
||||
|
||||
@@ -149,6 +149,15 @@ rclone help
|
||||
This will tremendously accelerate operations such as `rclone sync` or `rclone ncdu` by reducing the number
|
||||
of ListObjects calls that are made.
|
||||
|
||||
**Garage behind Cloudflare proxy:** when running Garage behind Cloudflare proxy, you might see `Response: error 403 Forbidden, Forbidden: Invalid signature` error in your garage logs or `AccessDenied: Forbidden: Invalid signature` error in rclone logs. Try adding `--s3-sign-accept-encoding=false` flag to your rclone command and see if the issue is resolved.
|
||||
|
||||
```bash
|
||||
# this throws an error
|
||||
rclone lsd garage:
|
||||
|
||||
# this should work
|
||||
rclone lsd --s3-sign-accept-encoding=false garage:
|
||||
```
|
||||
|
||||
## `s3cmd`
|
||||
|
||||
@@ -314,4 +323,3 @@ ls
|
||||
```
|
||||
|
||||
And through the web interface at http://[::1]:8080/web/client
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ Alpine Linux repositories (available since v3.17):
|
||||
apk add garage
|
||||
```
|
||||
|
||||
The default configuration file is installed to `/etc/garage.toml`. You can run
|
||||
Garage using: `rc-service garage start`. If you don't specify `rpc_secret`, it
|
||||
will be automatically replaced with a random string on the first start.
|
||||
The default configuration file is installed to `/etc/garage/garage.toml`. You can run
|
||||
Garage using: `rc-service garage start`.
|
||||
|
||||
If you don't specify `rpc_secret`, it will be automatically replaced with a random string on the first start.
|
||||
|
||||
Please note that this package is built without Consul discovery, Kubernetes
|
||||
discovery, OpenTelemetry exporter, and K2V features (K2V will be enabled once
|
||||
@@ -26,7 +27,11 @@ it's stable).
|
||||
|
||||
## Arch Linux
|
||||
|
||||
Garage is available in the [AUR](https://aur.archlinux.org/packages/garage).
|
||||
Garage is available in the official repositories under [extra](https://archlinux.org/packages/extra/x86_64/garage).
|
||||
|
||||
```bash
|
||||
pacman -S garage
|
||||
```
|
||||
|
||||
## FreeBSD
|
||||
|
||||
@@ -39,3 +44,9 @@ pkg install garage
|
||||
```bash
|
||||
nix-shell -p garage
|
||||
```
|
||||
|
||||
## conda-forge
|
||||
|
||||
```bash
|
||||
pixi global install garage
|
||||
```
|
||||
|
||||
@@ -20,10 +20,10 @@ sudo apt-get update
|
||||
sudo apt-get install build-essential
|
||||
```
|
||||
|
||||
## Building from source from the Gitea repository
|
||||
## Building from source from the Forgejo repository
|
||||
|
||||
The primary location for Garage's source code is the
|
||||
[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage),
|
||||
[Forgejo repository](https://git.deuxfleurs.fr/Deuxfleurs/garage),
|
||||
which contains all of the released versions as well as the code
|
||||
for the developpement of the next version.
|
||||
|
||||
@@ -85,11 +85,14 @@ The following feature flags are available in v0.8.0:
|
||||
| Feature flag | Enabled | Description |
|
||||
| ------------ | ------- | ----------- |
|
||||
| `bundled-libs` | *by default* | Use bundled version of sqlite3, zstd, lmdb and libsodium |
|
||||
| `system-libs` | optional | Use system version of sqlite3, zstd, lmdb and libsodium<br>if available (exclusive with `bundled-libs`, build using<br>`cargo build --no-default-features --features system-libs`) |
|
||||
| `consul-discovery` | optional | Enable automatic registration and discovery<br>of cluster nodes through the Consul API |
|
||||
| `fjall` | experimental | Enable using Fjall to store Garage's metadata |
|
||||
| `journald` | optional | Enable logging to systemd-journald with<br>`GARAGE_LOG_TO_JOURNALD=true` environment variable set |
|
||||
| `k2v` | optional | Enable the experimental K2V API (if used, all nodes on your<br>Garage cluster must have it enabled as well) |
|
||||
| `kubernetes-discovery` | optional | Enable automatic registration and discovery<br>of cluster nodes through the Kubernetes API |
|
||||
| `metrics` | *by default* | Enable collection of metrics in Prometheus format on the admin API |
|
||||
| `telemetry-otlp` | optional | Enable collection of execution traces using OpenTelemetry |
|
||||
| `syslog` | optional | Enable logging to Syslog |
|
||||
| `lmdb` | *by default* | Enable using LMDB to store Garage's metadata |
|
||||
| `metrics` | *by default* | Enable collection of metrics in Prometheus format on the admin API |
|
||||
| `sqlite` | *by default* | Enable using Sqlite3 to store Garage's metadata |
|
||||
| `syslog` | optional | Enable logging to Syslog with<br>`GARAGE_LOG_TO_SYSLOG=true` environment variable set |
|
||||
| `system-libs` | optional | Use system version of sqlite3, zstd, lmdb and libsodium<br>if available (exclusive with `bundled-libs`, build using<br>`cargo build --no-default-features --features system-libs`) |
|
||||
| `telemetry-otlp` | optional | Enable collection of execution traces using OpenTelemetry |
|
||||
|
||||
@@ -11,7 +11,7 @@ Firstly clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://git.deuxfleurs.fr/Deuxfleurs/garage
|
||||
cd garage/scripts/helm
|
||||
cd garage/script/helm
|
||||
```
|
||||
|
||||
Deploy with default options:
|
||||
|
||||
@@ -96,14 +96,14 @@ to store 2 TB of data in total.
|
||||
## Get a Docker image
|
||||
|
||||
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
||||
We encourage you to use a fixed tag (eg. `v2.1.0`) and not the `latest` tag.
|
||||
For this example, we will use the latest published version at the time of the writing which is `v2.1.0` but it's up to you
|
||||
We encourage you to use a fixed tag (eg. `v2.2.0`) and not the `latest` tag.
|
||||
For this example, we will use the latest published version at the time of the writing which is `v2.2.0` but it's up to you
|
||||
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
sudo docker pull dxflrs/garage:v2.1.0
|
||||
sudo docker pull dxflrs/garage:v2.2.0
|
||||
```
|
||||
|
||||
## Deploying and configuring Garage
|
||||
@@ -171,7 +171,7 @@ docker run \
|
||||
-v /etc/garage.toml:/etc/garage.toml \
|
||||
-v /var/lib/garage/meta:/var/lib/garage/meta \
|
||||
-v /var/lib/garage/data:/var/lib/garage/data \
|
||||
dxflrs/garage:v2.1.0
|
||||
dxflrs/garage:v2.2.0
|
||||
```
|
||||
|
||||
With this command line, Garage should be started automatically at each boot.
|
||||
@@ -185,7 +185,7 @@ If you want to use `docker-compose`, you may use the following `docker-compose.y
|
||||
version: "3"
|
||||
services:
|
||||
garage:
|
||||
image: dxflrs/garage:v2.1.0
|
||||
image: dxflrs/garage:v2.2.0
|
||||
network_mode: "host"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
||||
@@ -7,7 +7,7 @@ The main reason to add a reverse proxy in front of Garage is to provide TLS to y
|
||||
|
||||
In production you will likely need your certificates signed by a certificate authority.
|
||||
The most automated way is to use a provider supporting the [ACME protocol](https://datatracker.ietf.org/doc/html/rfc8555)
|
||||
such as [Let's Encrypt](https://letsencrypt.org/), [ZeroSSL](https://zerossl.com/) or [Buypass Go SSL](https://www.buypass.com/ssl/products/acme).
|
||||
such as [Let's Encrypt](https://letsencrypt.org/) or [ZeroSSL](https://zerossl.com/).
|
||||
|
||||
If you are only testing Garage, you can generate a self-signed certificate to follow the documentation:
|
||||
|
||||
|
||||
@@ -161,4 +161,7 @@ your recovery options are as follows:
|
||||
|
||||
- **Option 3: restoring a filesystem-level snapshot.** If you are using ZFS or
|
||||
BTRFS to snapshot your metadata partition, refer to their specific
|
||||
documentation on rolling back or copying files from an old snapshot.
|
||||
documentation on rolling back or copying files from an old snapshot.
|
||||
Note that, depending on the properties of the filesystem and of the DB engine,
|
||||
if these snapshots were taken during a write operation to the database, they may
|
||||
also be corrupted and thus unfit for recovery.
|
||||
|
||||
@@ -132,7 +132,7 @@ docker run \
|
||||
-v /path/to/garage.toml:/etc/garage.toml \
|
||||
-v /path/to/garage/meta:/var/lib/garage/meta \
|
||||
-v /path/to/garage/data:/var/lib/garage/data \
|
||||
dxflrs/garage:v2.1.0
|
||||
dxflrs/garage:v2.2.0
|
||||
```
|
||||
|
||||
Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`
|
||||
|
||||
@@ -25,7 +25,7 @@ db_engine = "lmdb"
|
||||
block_size = "1M"
|
||||
block_ram_buffer_max = "256MiB"
|
||||
block_max_concurrent_reads = 16
|
||||
|
||||
block_max_concurrent_writes_per_request =10
|
||||
lmdb_map_size = "1T"
|
||||
|
||||
compression_level = 1
|
||||
@@ -51,17 +51,20 @@ allow_punycode = false
|
||||
|
||||
[consul_discovery]
|
||||
api = "catalog"
|
||||
consul_http_addr = "http://127.0.0.1:8500"
|
||||
consul_http_addr = "https://127.0.0.1:8500"
|
||||
tls_skip_verify = false
|
||||
service_name = "garage-daemon"
|
||||
|
||||
ca_cert = "/etc/consul/consul-ca.crt"
|
||||
client_cert = "/etc/consul/consul-client.crt"
|
||||
client_key = "/etc/consul/consul-key.crt"
|
||||
|
||||
# for `agent` API mode, unset client_cert and client_key, and optionally enable `token`
|
||||
# token = "abcdef-01234-56789"
|
||||
tls_skip_verify = false
|
||||
|
||||
tags = [ "dns-enabled" ]
|
||||
meta = { dns-acl = "allow trusted" }
|
||||
|
||||
datacenters = ["dc1", "dc2", "dc3"]
|
||||
|
||||
[kubernetes_discovery]
|
||||
namespace = "garage"
|
||||
@@ -98,7 +101,8 @@ The following gives details about each available configuration option.
|
||||
Top-level configuration options, in alphabetical order:
|
||||
[`allow_punycode`](#allow_punycode),
|
||||
[`allow_world_readable_secrets`](#allow_world_readable_secrets),
|
||||
[`block_max_concurrent_reads`](`block_max_concurrent_reads),
|
||||
[`block_max_concurrent_reads`](#block_max_concurrent_reads),
|
||||
[`block_max_concurrent_writes_per_request`](#block_max_concurrent_writes_per_request),
|
||||
[`block_ram_buffer_max`](#block_ram_buffer_max),
|
||||
[`block_size`](#block_size),
|
||||
[`bootstrap_peers`](#bootstrap_peers),
|
||||
@@ -127,12 +131,14 @@ The `[consul_discovery]` section:
|
||||
[`client_cert`](#consul_client_cert_and_key),
|
||||
[`client_key`](#consul_client_cert_and_key),
|
||||
[`consul_http_addr`](#consul_http_addr),
|
||||
[`datacenters`](#consul_datacenters)
|
||||
[`meta`](#consul_tags_and_meta),
|
||||
[`service_name`](#consul_service_name),
|
||||
[`tags`](#consul_tags_and_meta),
|
||||
[`tls_skip_verify`](#consul_tls_skip_verify),
|
||||
[`token`](#consul_token).
|
||||
|
||||
|
||||
The `[kubernetes_discovery]` section:
|
||||
[`namespace`](#kube_namespace),
|
||||
[`service_name`](#kube_service_name),
|
||||
@@ -337,7 +343,7 @@ Since `v0.8.0`, Garage can use alternative storage backends as follows:
|
||||
| --------- | ----------------- | ------------- |
|
||||
| [LMDB](https://www.symas.com/lmdb) (since `v0.8.0`, default since `v0.9.0`) | `"lmdb"` | `<metadata_dir>/db.lmdb/` |
|
||||
| [Sqlite](https://sqlite.org) (since `v0.8.0`) | `"sqlite"` | `<metadata_dir>/db.sqlite` |
|
||||
| [Fjall](https://github.com/fjall-rs/fjall) (**experimental support** since `v1.3.0`) | `"fjall"` | `<metadata_dir>/db.fjall/` |
|
||||
| [Fjall](https://github.com/fjall-rs/fjall) (**experimental support** since `v1.3.0`/`v2.1.0`) | `"fjall"` | `<metadata_dir>/db.fjall/` |
|
||||
| [Sled](https://sled.rs) (old default, removed since `v1.0`) | `"sled"` | `<metadata_dir>/db/` |
|
||||
|
||||
Sled was supported until Garage v0.9.x, and was removed in Garage v1.0.
|
||||
@@ -346,8 +352,16 @@ old Sled metadata databases to another engine.
|
||||
|
||||
Performance characteristics of the different DB engines are as follows:
|
||||
|
||||
- LMDB: the recommended database engine for high-performance distributed clusters.
|
||||
LMDB works very well, but is known to have the following limitations:
|
||||
- **LMDB:** the recommended database engine for high-performance distributed clusters
|
||||
with `replication_factor` ≥ 2.
|
||||
LMDB works well, but is known to have the following limitations:
|
||||
|
||||
- LMDB is prone to database corruption after an unclean shutdown (e.g. a process kill
|
||||
or a power outage). It is recommended to configure
|
||||
[`metadata_auto_snapshot_interval`](#metadata_auto_snapshot_interval) to be
|
||||
able to easily recover from this situation. With `replication_factor` ≥ 2,
|
||||
metadata can also be reconstructed from remote nodes upon corruption
|
||||
(see [Recovering from failures](@/documentation/operations/recovering.md#corrupted_meta)).
|
||||
|
||||
- The data format of LMDB is not portable between architectures, so for
|
||||
instance the Garage database of an x86-64 node cannot be moved to an ARM64
|
||||
@@ -357,30 +371,21 @@ LMDB works very well, but is known to have the following limitations:
|
||||
node to very small database sizes due to how LMDB works; it is therefore
|
||||
not recommended.
|
||||
|
||||
- Several users have reported corrupted LMDB database files after an unclean
|
||||
shutdown (e.g. a power outage). This situation can generally be recovered
|
||||
from if your cluster is geo-replicated (by rebuilding your metadata db from
|
||||
other nodes), or if you have saved regular snapshots at the filesystem
|
||||
level.
|
||||
|
||||
- Keys in LMDB are limited to 511 bytes. This limit translates to limits on
|
||||
object keys in S3 and sort keys in K2V that are limted to 479 bytes.
|
||||
|
||||
- Sqlite: Garage supports Sqlite as an alternative storage backend for
|
||||
metadata, which does not have the issues listed above for LMDB.
|
||||
On versions 0.8.x and earlier, Sqlite should be avoided due to abysmal
|
||||
performance, which was fixed with the addition of `metadata_fsync`.
|
||||
Sqlite is still probably slower than LMDB due to the way we use it,
|
||||
so it is not the best choice for high-performance storage clusters,
|
||||
but it should work fine in many cases.
|
||||
- **Sqlite:** Garage supports Sqlite as an alternative storage backend for
|
||||
metadata, which does not have the issues listed above for LMDB. Sqlite is
|
||||
slower than LMDB, so it is not the best choice for high-performance storage
|
||||
clusters.
|
||||
|
||||
- Fjall: a storage engine based on LSM trees, which theoretically allow for
|
||||
- **Fjall:** a storage engine based on LSM trees, which theoretically allow for
|
||||
higher write throughput than other storage engines that are based on B-trees.
|
||||
Using Fjall could potentially improve Garage's performance significantly in
|
||||
write-heavy workloads. **Support for Fjall is experimental at this point**,
|
||||
we have added it to Garage for evaluation purposes only. **Do not use it for
|
||||
production-critical workloads.**
|
||||
|
||||
we have added it to Garage for evaluation purposes only. **Use it only with
|
||||
test data, and report any issues to our bug tracker. Do not use it for
|
||||
production workloads.**
|
||||
|
||||
It is possible to convert Garage's metadata directory from one format to another
|
||||
using the `garage convert-db` command, which should be used as follows:
|
||||
@@ -439,7 +444,8 @@ if geographical replication is used.
|
||||
#### `metadata_auto_snapshot_interval` (since `v0.9.4`) {#metadata_auto_snapshot_interval}
|
||||
|
||||
If this value is set, Garage will automatically take a snapshot of the metadata
|
||||
DB file at a regular interval and save it in the metadata directory.
|
||||
DB file at a regular interval and save it in the metadata directory,
|
||||
or in [`metadata_snapshots_dir`](#metadata_snapshots_dir) if it is set.
|
||||
This parameter can take any duration string that can be parsed by
|
||||
the [`parse_duration`](https://docs.rs/parse_duration/latest/parse_duration/#syntax) crate.
|
||||
|
||||
@@ -448,14 +454,19 @@ corrupted, for instance after an unclean shutdown. See [this
|
||||
page](@/documentation/operations/recovering.md#corrupted_meta) for details.
|
||||
Garage keeps only the two most recent snapshots of the metadata DB and deletes
|
||||
older ones automatically.
|
||||
You can also create metadata snapshots manually at any point using the
|
||||
`garage meta snapshot` command.
|
||||
|
||||
Using snapshots created by Garage is the best option to make snapshots of your
|
||||
node's metadata for potential recovery, as they are guaranteed to be clean and
|
||||
consistent, contrarily to filesystem-level snapshots that may be taken while
|
||||
some writes are in-flight and thus might be corrupted.
|
||||
|
||||
Note that taking a metadata snapshot is a relatively intensive operation as the
|
||||
entire data file is copied. A snapshot being taken might have performance
|
||||
impacts on the Garage node while it is running. If the cluster is under heavy
|
||||
write load when a snapshot operation is running, this might also cause the
|
||||
database file to grow in size significantly as pages cannot be recycled easily.
|
||||
For this reason, it might be better to use filesystem-level snapshots instead
|
||||
if possible.
|
||||
|
||||
#### `disable_scrub` {#disable_scrub}
|
||||
|
||||
@@ -549,6 +560,14 @@ metric in Prometheus: a non-zero number of such events indicates an I/O
|
||||
bottleneck on HDD read speed.
|
||||
|
||||
|
||||
#### `block_max_concurrent_writes_per_request` (since `v1.3.1` / `v2.2.0`) {#block_max_concurrent_writes_per_request}
|
||||
|
||||
This parameter is designed to adapt to the concurrent write performance of
|
||||
different storage media. Maximum number of parallel block writes per put request.
|
||||
Higher values may improve throughput but increase memory usage.
|
||||
|
||||
Default value: 3. Recommended values: 10-30 for NVMe, 3-10 for spinning HDD.
|
||||
|
||||
#### `lmdb_map_size` {#lmdb_map_size}
|
||||
|
||||
This parameters can be used to set the map size used by LMDB,
|
||||
@@ -721,6 +740,18 @@ node_prefix "" {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### `datacenters` {#consul_datacenters}
|
||||
|
||||
Optional list of datacenters that allow garage to do service discovery when Consul is configured in WAN federation.
|
||||
|
||||
Example: `datacenters = ["dc1", "dc2", "dc3"]`
|
||||
|
||||
In a WAN configuration, by default the Consul services API only responds with
|
||||
local LAN services. When a list of datacenters is specified using this option,
|
||||
Garage will query the consul server API by datacenter directly, allowing for
|
||||
Garage to discover nodes across the Consul WAN.
|
||||
|
||||
#### `tags` and `meta` {#consul_tags_and_meta}
|
||||
|
||||
Additional list of tags and map of service meta to add during service registration.
|
||||
|
||||
@@ -27,7 +27,7 @@ Feel free to open a PR to suggest fixes this table. Minio is missing because the
|
||||
|
||||
| Feature | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) |
|
||||
|------------------------------|----------------------------------|-----------------|---------------|---------|-----|
|
||||
| [signature v2](https://docs.aws.amazon.com/general/latest/gr/signature-version-2.html) (deprecated) | ❌ Missing | ✅ | ✅ | ✅ | ✅ |
|
||||
| [signature v2](https://docs.aws.amazon.com/AmazonS3/latest/API/Appendix-Sigv2.html) (deprecated) | ❌ Missing | ✅ | ✅ | ✅ | ✅ |
|
||||
| [signature v4](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) | ✅ Implemented | ✅ | ✅ | ❌ | ✅ |
|
||||
| [URL path-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#path-style-access) (eg. `host.tld/bucket/key`) | ✅ Implemented | ✅ | ✅ | ❓| ✅ |
|
||||
| [URL vhost-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access) URL (eg. `bucket.host.tld/key`) | ✅ Implemented | ❌| ✅| ✅ | ✅ |
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
*
|
||||
|
||||
!*.txt
|
||||
!*.md
|
||||
|
||||
!assets
|
||||
|
||||
!.gitignore
|
||||
!*.svg
|
||||
!*.png
|
||||
!*.jpg
|
||||
!*.tex
|
||||
!Makefile
|
||||
!.gitignore
|
||||
!assets/*.drawio.pdf
|
||||
|
||||
!talk.pdf
|
||||
@@ -0,0 +1,19 @@
|
||||
ASSETS=../assets/lattice/lattice1.pdf_tex \
|
||||
../assets/lattice/lattice2.pdf_tex \
|
||||
../assets/lattice/lattice3.pdf_tex \
|
||||
../assets/lattice/lattice4.pdf_tex \
|
||||
../assets/lattice/lattice5.pdf_tex \
|
||||
../assets/lattice/lattice6.pdf_tex \
|
||||
../assets/lattice/lattice7.pdf_tex \
|
||||
../assets/lattice/lattice8.pdf_tex \
|
||||
../assets/logos/deuxfleurs.pdf \
|
||||
../assets/timeline-22-24.pdf
|
||||
|
||||
talk.pdf: talk.tex $(ASSETS)
|
||||
pdflatex talk.tex
|
||||
|
||||
%.pdf: %.svg
|
||||
inkscape -D -z --file=$^ --export-pdf=$@
|
||||
|
||||
%.pdf_tex: %.svg
|
||||
inkscape -D -z --file=$^ --export-pdf=$@ --export-latex
|
||||
@@ -0,0 +1,702 @@
|
||||
\nonstopmode
|
||||
\documentclass[aspectratio=169,xcolor={svgnames}]{beamer}
|
||||
\usepackage[utf8]{inputenc}
|
||||
% \usepackage[frenchb]{babel}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{mathtools}
|
||||
\usepackage{breqn}
|
||||
\usepackage{multirow}
|
||||
\usetheme{boxes}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{import}
|
||||
\usepackage{adjustbox}
|
||||
\usepackage[absolute,overlay]{textpos}
|
||||
%\useoutertheme[footline=authortitle,subsection=false]{miniframes}
|
||||
%\useoutertheme[footline=authorinstitute,subsection=false]{miniframes}
|
||||
\useoutertheme{infolines}
|
||||
\setbeamertemplate{headline}{}
|
||||
|
||||
\beamertemplatenavigationsymbolsempty
|
||||
|
||||
\definecolor{TitleOrange}{RGB}{255,137,0}
|
||||
\setbeamercolor{title}{fg=TitleOrange}
|
||||
\setbeamercolor{frametitle}{fg=TitleOrange}
|
||||
|
||||
\definecolor{ListOrange}{RGB}{255,145,5}
|
||||
\setbeamertemplate{itemize item}{\color{ListOrange}$\blacktriangleright$}
|
||||
|
||||
\definecolor{verygrey}{RGB}{70,70,70}
|
||||
\setbeamercolor{normal text}{fg=verygrey}
|
||||
|
||||
|
||||
\usepackage{tabu}
|
||||
\usepackage{multicol}
|
||||
\usepackage{vwcol}
|
||||
\usepackage{stmaryrd}
|
||||
\usepackage{graphicx}
|
||||
|
||||
\usepackage[normalem]{ulem}
|
||||
|
||||
\AtBeginSection[]{
|
||||
\begin{frame}
|
||||
\vfill
|
||||
\centering
|
||||
\begin{beamercolorbox}[sep=8pt,center,shadow=true,rounded=true]{title}
|
||||
\usebeamerfont{title}\insertsectionhead\par%
|
||||
\end{beamercolorbox}
|
||||
\vfill
|
||||
\end{frame}
|
||||
}
|
||||
|
||||
\title{Garage, an S3 backend as reliable as possible}
|
||||
\author{Garage Authors}
|
||||
\date{JoSy S3, 2025-10-08}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\begin{frame}
|
||||
\centering
|
||||
\includegraphics[width=.3\linewidth]{../../sticker/Garage.png}
|
||||
\vspace{1em}
|
||||
|
||||
{\large\bf Garage, an S3 backend as reliable as possible}
|
||||
\vspace{1em}
|
||||
|
||||
\url{https://garagehq.deuxfleurs.fr/}\\
|
||||
\url{mailto:garagehq@deuxfleurs.fr}\\
|
||||
\texttt{\#garage:deuxfleurs.fr} on Matrix
|
||||
\end{frame}
|
||||
|
||||
|
||||
\section{Meet Garage}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{A non-profit initiative}
|
||||
|
||||
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.5\linewidth, valign=t]{../assets/logos/deuxfleurs.pdf}
|
||||
\end{column}
|
||||
\begin{column}{.8\textwidth}
|
||||
\textbf{Part of a degrowth initiative}\\
|
||||
Garage has been created at Deuxfleurs where we experiment running Internet services without datacenter on commodity and refurbished hardware.
|
||||
\end{column}
|
||||
|
||||
\end{columns}
|
||||
\vspace{2em}
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.5\linewidth, valign=t]{../assets/community.png}
|
||||
\end{column}
|
||||
\begin{column}{.8\textwidth}
|
||||
\textbf{Developed by a community}\\
|
||||
{\small Some recent contributors: Arthur C, Charles H, dongdigua, Etienne L, Jonah A, Julien K, Lapineige, MagicRR, Milas B, Niklas M, RockWolf, Schwitzd, trinity-1686a, Xavier S, babykart, Baptiste J, eddster2309, James O'C, Joker9944, Maximilien R, Renjaya RZ, Yureka...}
|
||||
\end{column}
|
||||
|
||||
\end{columns}
|
||||
\vspace{2em}
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.5\linewidth, valign=t]{../assets/logos/AGPLv3_Logo.png}
|
||||
\end{column}
|
||||
\begin{column}{.8\textwidth}
|
||||
\textbf{Owned by nobody, open-core is impossible, zero VC money}\\
|
||||
AGPL + no Contributor License Agreement = Garage ownership spreads among hundredth of contributors.
|
||||
\end{column}
|
||||
|
||||
\end{columns}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Getting support for Garage}
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.4\linewidth, valign=t]{../assets/alex.jpg}
|
||||
\end{column}
|
||||
\begin{column}{.4\textwidth}
|
||||
\textbf{Alex Auvolat}\\
|
||||
PhD; co-founder of Deuxfleurs\\
|
||||
Garage maintainer, Freelance
|
||||
\end{column}
|
||||
\begin{column}{.3\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.4\linewidth, valign=t]{../assets/support.png}
|
||||
\end{column}
|
||||
\begin{column}{.1\textwidth}
|
||||
~
|
||||
\end{column}
|
||||
\end{columns}
|
||||
\vspace{2em}
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.4\linewidth, valign=t]{../assets/quentin.jpg}
|
||||
\end{column}
|
||||
\begin{column}{.4\textwidth}
|
||||
\textbf{Quentin Dufour}\\
|
||||
PhD; co-founder of Deuxfleurs\\
|
||||
Garage contributor, Freelance
|
||||
\end{column}
|
||||
\begin{column}{.4\textwidth}
|
||||
For support requests, write at: \\
|
||||
\url{garagehq@deuxfleurs.fr}
|
||||
\end{column}
|
||||
\end{columns}
|
||||
\vspace{2em}
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.4\linewidth, valign=t]{../assets/armael.jpg}
|
||||
\end{column}
|
||||
\begin{column}{.4\textwidth}
|
||||
\textbf{Armaël Guéneau}\\
|
||||
PhD; member of Deuxfleurs\\
|
||||
Garage contributor, Freelance
|
||||
\end{column}
|
||||
\begin{column}{.4\textwidth}
|
||||
Eligible: email support, architecture design, specific feature development, etc.
|
||||
\end{column}
|
||||
\end{columns}
|
||||
|
||||
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Our initial goal}
|
||||
|
||||
\centering
|
||||
\Large
|
||||
|
||||
Being a self-sovereign community to be free of our degrowth choice
|
||||
|
||||
$\big\downarrow$
|
||||
|
||||
As web citizens, datacenters are big black boxes. \\
|
||||
We want to leave them to autonoumously manage our servers.
|
||||
|
||||
$\big\downarrow$
|
||||
|
||||
We want reliable services without relying on dedicated hardware or places.
|
||||
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Building a resilient system with cheap stuff}
|
||||
|
||||
\only<1,4-7>{
|
||||
\begin{itemize}
|
||||
\item \textcolor<5->{gray}{Commodity hardware (e.g. old desktop PCs)\\
|
||||
\vspace{.5em}
|
||||
\visible<4->{{\footnotesize (can die at any time)}}}
|
||||
\vspace{1.5em}
|
||||
\item<5-> \textcolor<7->{gray}{Regular Internet (e.g. FTTB, FTTH) and power grid connections\\
|
||||
\vspace{.5em}
|
||||
\visible<6->{{\footnotesize (can be unavailable randomly)}}}
|
||||
\vspace{1.5em}
|
||||
\item<7-> \textbf{Geographical redundancy} (multi-site replication)
|
||||
\end{itemize}
|
||||
}
|
||||
\only<2>{
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/neptune.jpg}
|
||||
\end{center}
|
||||
}
|
||||
\only<3>{
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/atuin.jpg}
|
||||
\end{center}
|
||||
}
|
||||
\only<8>{
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/inframap_jdll2023.pdf}
|
||||
\end{center}
|
||||
}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Object storage: a crucial component}
|
||||
\begin{center}
|
||||
\includegraphics[height=6em]{../assets/logos/Amazon-S3.jpg}
|
||||
\hspace{3em}
|
||||
\visible<2->{\includegraphics[height=5em]{../assets/logos/minio.png}}
|
||||
\hspace{3em}
|
||||
\visible<3>{\includegraphics[height=6em]{../../logo/garage_hires_crop.png}}
|
||||
\end{center}
|
||||
\vspace{1em}
|
||||
S3: a de-facto standard, many compatible applications
|
||||
|
||||
\vspace{1em}
|
||||
\visible<2->{MinIO is self-hostable but not suited for geo-distributed deployments}
|
||||
|
||||
\vspace{1em}
|
||||
\visible<3->{\textbf{Garage is a self-hosted drop-in replacement for the Amazon S3 object store}}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{CRDTs / weak consistency instead of consensus}
|
||||
|
||||
\underline{Internally, Garage uses only CRDTs} (conflict-free replicated data types)
|
||||
|
||||
\vspace{2em}
|
||||
Why not Raft, Paxos, ...? Issues of consensus algorithms:
|
||||
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item<2-> \textbf{Software complexity}
|
||||
\vspace{1em}
|
||||
\item<3-> \textbf{Performance issues:}
|
||||
\vspace{.5em}
|
||||
\begin{itemize}
|
||||
\item<4-> The leader is a \textbf{bottleneck} for all requests\\
|
||||
\vspace{.5em}
|
||||
\item<5-> \textbf{Sensitive to higher latency} between nodes
|
||||
\vspace{.5em}
|
||||
\item<6-> \textbf{Takes time to reconverge} when disrupted (e.g. node going down)
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{The data model of object storage}
|
||||
Object storage is basically a \textbf{key-value store}:
|
||||
\vspace{.5em}
|
||||
|
||||
{\scriptsize
|
||||
\begin{center}
|
||||
\begin{tabular}{|l|p{7cm}|}
|
||||
\hline
|
||||
\textbf{Key: file path + name} & \textbf{Value: file data + metadata} \\
|
||||
\hline
|
||||
\hline
|
||||
\texttt{index.html} &
|
||||
\texttt{Content-Type: text/html; charset=utf-8} \newline
|
||||
\texttt{Content-Length: 24929} \newline
|
||||
\texttt{<binary blob>} \\
|
||||
\hline
|
||||
\texttt{img/logo.svg} &
|
||||
\texttt{Content-Type: text/svg+xml} \newline
|
||||
\texttt{Content-Length: 13429} \newline
|
||||
\texttt{<binary blob>} \\
|
||||
\hline
|
||||
\texttt{download/index.html} &
|
||||
\texttt{Content-Type: text/html; charset=utf-8} \newline
|
||||
\texttt{Content-Length: 26563} \newline
|
||||
\texttt{<binary blob>} \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
}
|
||||
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item<2> Maps well to CRDT data types
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Performance gains in practice}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/perf/endpoint_latency_0.7_0.8_minio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
% ======================================== OPERATING
|
||||
% ======================================== OPERATING
|
||||
% ======================================== OPERATING
|
||||
|
||||
|
||||
\section{Production clusters}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Deployment kinds}
|
||||
|
||||
\includegraphics[width=.9\linewidth]{../assets/cluster_kind.png}
|
||||
\vspace{1em}
|
||||
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{How big they are?}
|
||||
|
||||
\includegraphics[width=.9\linewidth]{../assets/cluster_size.png}
|
||||
\vspace{1em}
|
||||
|
||||
\textit{"Petabyte storage setup for a video site. Nginx as CDN in-front using garage-s3-website feature. Each storage node has ~64TB storage with raid10, no replication within garage. 25gbit nic. haproxy to loadbalance across 5 nodes. mostly reads with very few writes."}
|
||||
|
||||
\vspace{1em}
|
||||
\textit{"We currently manage 7 Garage nodes, 28TB total storage, 6M blocks for 3M objects and 4TB of object data. We have been running Garage in production for 2.5 years."}
|
||||
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Operating Garage}
|
||||
\begin{center}
|
||||
\only<1-2>{
|
||||
\includegraphics[width=.9\linewidth]{../assets/screenshots/garage_status_0.10.png}
|
||||
\\\vspace{1em}
|
||||
\visible<2>{\includegraphics[width=.9\linewidth]{../assets/screenshots/garage_status_unhealthy_0.10.png}}
|
||||
}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Garage's architecture}
|
||||
\begin{center}
|
||||
\only<1>{\includegraphics[width=.45\linewidth]{../assets/garage.drawio.pdf}}%
|
||||
\only<2>{\includegraphics[width=.6\linewidth]{../assets/garage_sync.drawio.pdf}}%
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Digging deeper}
|
||||
\begin{center}
|
||||
\only<1>{\includegraphics[width=.9\linewidth]{../assets/screenshots/garage_stats_0.10.png}}
|
||||
\only<2>{\includegraphics[width=.5\linewidth]{../assets/screenshots/garage_worker_list_0.10.png}}
|
||||
\only<3>{\includegraphics[width=.6\linewidth]{../assets/screenshots/garage_worker_param_0.10.png}}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Potential limitations and bottlenecks}
|
||||
\begin{itemize}
|
||||
\item Global:
|
||||
\begin{itemize}
|
||||
\item Max. $\sim$100 nodes per cluster (excluding gateways)
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
\item Metadata:
|
||||
\begin{itemize}
|
||||
\item One big bucket = bottleneck, object list on 3 nodes only
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
\item Block manager:
|
||||
\begin{itemize}
|
||||
\item Lots of small files on disk
|
||||
\item Processing the resync queue can be slow
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Deployment advice for very large clusters}
|
||||
\begin{itemize}
|
||||
\item Metadata storage:
|
||||
\begin{itemize}
|
||||
\item ZFS mirror (x2) on fast NVMe
|
||||
\item Use LMDB storage engine
|
||||
\end{itemize}
|
||||
\vspace{.5em}
|
||||
\item Data block storage:
|
||||
\begin{itemize}
|
||||
\item Use Garage's native multi-HDD support
|
||||
\item XFS on individual drives
|
||||
\item Increase block size (1MB $\to$ 10MB, requires more RAM and good networking)
|
||||
\item Tune \texttt{resync-tranquility} and \texttt{resync-worker-count} dynamically
|
||||
\end{itemize}
|
||||
\vspace{.5em}
|
||||
\item Other :
|
||||
\begin{itemize}
|
||||
\item Split data over several buckets
|
||||
\item Use less than 100 storage nodes
|
||||
\item Use gateway nodes
|
||||
\end{itemize}
|
||||
\vspace{.5em}
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Focus on Deuxfleurs}
|
||||
|
||||
Host institutional websites, partnership with a web agency.
|
||||
Matrix media backend.
|
||||
|
||||
Plan to use it as an email backend for an internally developed email server.
|
||||
|
||||
\end{frame}
|
||||
|
||||
|
||||
% ======================================== TIMELINE
|
||||
% ======================================== TIMELINE
|
||||
% ======================================== TIMELINE
|
||||
|
||||
\section{Recent developments}
|
||||
|
||||
% ====================== v0.7.0 ===============================
|
||||
|
||||
\begin{frame}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/tl.drawio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{April 2022 - Garage v0.7.0}
|
||||
Focus on \underline{observability and ecosystem integration}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item \textbf{Monitoring:} metrics and traces, using OpenTelemetry
|
||||
\vspace{1em}
|
||||
\item Replication modes with 1 or 2 copies / weaker consistency
|
||||
\vspace{1em}
|
||||
\item Kubernetes integration for node discovery
|
||||
\vspace{1em}
|
||||
\item Admin API (v0.7.2)
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Metrics (Prometheus + Grafana)}
|
||||
\begin{center}
|
||||
\includegraphics[width=.9\linewidth]{../assets/screenshots/grafana_dashboard.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Traces (Jaeger)}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/screenshots/jaeger_listobjects.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
% ====================== v0.8.0 ===============================
|
||||
|
||||
\begin{frame}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/tl.drawio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{November 2022 - Garage v0.8.0}
|
||||
Focus on \underline{performance}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item \textbf{Alternative metadata DB engines} (LMDB, Sqlite)
|
||||
\vspace{1em}
|
||||
\item \textbf{Performance improvements:} block streaming, various optimizations...
|
||||
\vspace{1em}
|
||||
\item Bucket quotas (max size, max \#objects)
|
||||
\vspace{1em}
|
||||
\item Quality of life improvements, observability, etc.
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{About metadata DB engines}
|
||||
\textbf{Issues with Sled:}
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item Huge files on disk
|
||||
\vspace{.5em}
|
||||
\item Unpredictable performance, especially on HDD
|
||||
\vspace{.5em}
|
||||
\item API limitations
|
||||
\vspace{.5em}
|
||||
\item Not actively maintained
|
||||
\end{itemize}
|
||||
|
||||
\vspace{2em}
|
||||
\textbf{LMDB:} very stable, good performance, file size is reasonable\\
|
||||
\textbf{Sqlite} also available as a second choice
|
||||
|
||||
\vspace{1em}
|
||||
Sled will be removed in Garage v1.0
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{DB engine performance comparison}
|
||||
\begin{center}
|
||||
\includegraphics[width=.6\linewidth]{../assets/perf/db_engine.png}
|
||||
\end{center}
|
||||
NB: Sqlite was slow due to synchronous mode, now configurable
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Block streaming}
|
||||
\begin{center}
|
||||
\only<1>{\includegraphics[width=.8\linewidth]{../assets/schema-streaming-1.png}}
|
||||
\only<2>{\includegraphics[width=.8\linewidth]{../assets/schema-streaming-2.png}}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{TTFB benchmark}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/perf/ttfb.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Throughput benchmark}
|
||||
\begin{center}
|
||||
\includegraphics[width=.7\linewidth]{../assets/perf/io-0.7-0.8-minio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
% ====================== v0.9.0 ===============================
|
||||
|
||||
\begin{frame}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/tl.drawio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{October 2023 - Garage v0.9.0}
|
||||
Focus on \underline{streamlining \& usability}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item Support multiple HDDs per node
|
||||
\vspace{1em}
|
||||
\item S3 compatibility:
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item support basic lifecycle configurations
|
||||
\vspace{.5em}
|
||||
\item allow for multipart upload part retries
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
\item LMDB by default, deprecation of Sled
|
||||
\vspace{1em}
|
||||
\item New layout computation algorithm
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Layout computation}
|
||||
\begin{overprint}
|
||||
\onslide<1>
|
||||
\begin{center}
|
||||
\includegraphics[width=\linewidth, trim=0 0 0 -4cm]{../assets/screenshots/garage_status_0.9_prod_zonehl.png}
|
||||
\end{center}
|
||||
\onslide<2>
|
||||
\begin{center}
|
||||
\includegraphics[width=.7\linewidth]{../assets/map.png}
|
||||
\end{center}
|
||||
\end{overprint}
|
||||
\vspace{1em}
|
||||
Garage stores replicas on different zones when possible
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{What a "layout" is}
|
||||
\textbf{A layout is a precomputed index table:}
|
||||
\vspace{1em}
|
||||
|
||||
{\footnotesize
|
||||
\begin{center}
|
||||
\begin{tabular}{|l|l|l|l|}
|
||||
\hline
|
||||
\textbf{Partition} & \textbf{Node 1} & \textbf{Node 2} & \textbf{Node 3} \\
|
||||
\hline
|
||||
\hline
|
||||
Partition 0 & df-ymk (bespin) & Abricot (scorpio) & Courgette (neptune) \\
|
||||
\hline
|
||||
Partition 1 & Ananas (scorpio) & Courgette (neptune) & df-ykl (bespin) \\
|
||||
\hline
|
||||
Partition 2 & df-ymf (bespin) & Celeri (neptune) & Abricot (scorpio) \\
|
||||
\hline
|
||||
\hspace{1em}$\vdots$ & \hspace{1em}$\vdots$ & \hspace{1em}$\vdots$ & \hspace{1em}$\vdots$ \\
|
||||
\hline
|
||||
Partition 255 & Concombre (neptune) & df-ykl (bespin) & Abricot (scorpio) \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
}
|
||||
|
||||
\vspace{2em}
|
||||
\visible<2->{
|
||||
The index table is built centrally using an optimal algorithm,\\
|
||||
then propagated to all nodes
|
||||
}
|
||||
|
||||
\vspace{1em}
|
||||
\visible<3->{
|
||||
\footnotesize
|
||||
Oulamara, M., \& Auvolat, A. (2023). \emph{An algorithm for geo-distributed and redundant storage in Garage}.\\ arXiv preprint arXiv:2302.13798.
|
||||
}
|
||||
\end{frame}
|
||||
|
||||
|
||||
|
||||
% ====================== v1.0.0 ===============================
|
||||
|
||||
\begin{frame}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/tl.drawio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{April 2024 - Garage v1.0.0}
|
||||
Focus on \underline{consistency, security \& stability}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item Fix consistency issues when reshuffling data (Jepsen testing)
|
||||
\vspace{1em}
|
||||
\item \textbf{Security audit} by Radically Open Security
|
||||
\vspace{1em}
|
||||
\item Misc. S3 features (SSE-C, checksums, ...) and compatibility fixes
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
% ====================== v2.0.0 ===============================
|
||||
|
||||
\begin{frame}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/tl.drawio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Garage v2.0.0}
|
||||
Focus on \underline{}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item TODO
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Currently funding...}
|
||||
|
||||
\textit{...}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{We run community surveys}
|
||||
\begin{center}
|
||||
\includegraphics[width=.6\linewidth]{../assets/survey_requested_features.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
% ======================================== END
|
||||
% ======================================== END
|
||||
% ======================================== END
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Where to find us}
|
||||
\begin{center}
|
||||
\includegraphics[width=.25\linewidth]{../../logo/garage_hires.png}\\
|
||||
\vspace{-1em}
|
||||
\url{https://garagehq.deuxfleurs.fr/}\\
|
||||
\url{mailto:garagehq@deuxfleurs.fr}\\
|
||||
\texttt{\#garage:deuxfleurs.fr} on Matrix
|
||||
|
||||
\vspace{1.5em}
|
||||
\includegraphics[width=.06\linewidth]{../assets/logos/rust_logo.png}
|
||||
\includegraphics[width=.13\linewidth]{../assets/logos/AGPLv3_Logo.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\end{document}
|
||||
|
||||
%% vim: set ts=4 sw=4 tw=0 noet spelllang=en :
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 183 KiB |
@@ -12,16 +12,17 @@
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "6fe74265bbb6d016d663b1091f015e2976c4a527",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"locked": {
|
||||
"lastModified": 1717312683,
|
||||
"narHash": "sha256-FrlieJH50AuvagamEvWMIE6D2OAnERuDboFDYAED/dE=",
|
||||
"lastModified": 1761640442,
|
||||
"narHash": "sha256-AtrEP6Jmdvrqiv4x2xa5mrtaIp3OEe8uBYCDZDS+hu8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "flake-compat",
|
||||
"rev": "38fd3954cf65ce6faf3d0d45cd26059e059f07ea",
|
||||
"rev": "4a56054d8ffc173222d09dad23adf4ba946c8884",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -50,17 +51,17 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1747825515,
|
||||
"narHash": "sha256-BWpMQymVI73QoKZdcVCxUCCK3GNvr/xa2Dc4DM1o2BE=",
|
||||
"lastModified": 1763977559,
|
||||
"narHash": "sha256-g4MKqsIRy5yJwEsI+fYODqLUnAqIY4kZai0nldAP6EM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cd2812de55cf87df88a9e09bf3be1ce63d50c1a6",
|
||||
"rev": "cfe2c7d5b5d3032862254e68c37a6576b633d632",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cd2812de55cf87df88a9e09bf3be1ce63d50c1a6",
|
||||
"rev": "cfe2c7d5b5d3032862254e68c37a6576b633d632",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
@@ -80,17 +81,17 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1738549608,
|
||||
"narHash": "sha256-GdyT9QEUSx5k/n8kILuNy83vxxdyUfJ8jL5mMpQZWfw=",
|
||||
"lastModified": 1763952169,
|
||||
"narHash": "sha256-+PeDBD8P+NKauH+w7eO/QWCIp8Cx4mCfWnh9sJmy9CM=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "35c6f8c4352f995ecd53896200769f80a3e8f22d",
|
||||
"rev": "ab726555a9a72e6dc80649809147823a813fa95b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "35c6f8c4352f995ecd53896200769f80a3e8f22d",
|
||||
"rev": "ab726555a9a72e6dc80649809147823a813fa95b",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
description =
|
||||
"Garage, an S3-compatible distributed object store for self-hosted deployments";
|
||||
|
||||
# Nixpkgs 25.05 as of 2025-05-22
|
||||
# Nixpkgs 25.05 as of 2025-11-24
|
||||
inputs.nixpkgs.url =
|
||||
"github:NixOS/nixpkgs/cd2812de55cf87df88a9e09bf3be1ce63d50c1a6";
|
||||
"github:NixOS/nixpkgs/cfe2c7d5b5d3032862254e68c37a6576b633d632";
|
||||
|
||||
# Rust overlay as of 2025-02-03
|
||||
# Rust overlay as of 2025-11-24
|
||||
inputs.rust-overlay.url =
|
||||
"github:oxalica/rust-overlay/35c6f8c4352f995ecd53896200769f80a3e8f22d";
|
||||
"github:oxalica/rust-overlay/ab726555a9a72e6dc80649809147823a813fa95b";
|
||||
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
inputs.crane.url = "github:ipetkov/crane";
|
||||
# Crane as of 2025-01-24
|
||||
inputs.crane.url = "github:ipetkov/crane/6fe74265bbb6d016d663b1091f015e2976c4a527";
|
||||
|
||||
inputs.flake-compat.url = "github:nix-community/flake-compat";
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
@@ -30,6 +31,10 @@
|
||||
inherit system nixpkgs crane rust-overlay extraTestEnv;
|
||||
release = false;
|
||||
}).garage-test;
|
||||
lints = (compile {
|
||||
inherit system nixpkgs crane rust-overlay;
|
||||
release = false;
|
||||
});
|
||||
in
|
||||
{
|
||||
packages = {
|
||||
@@ -56,6 +61,10 @@
|
||||
tests-fjall = testWith {
|
||||
GARAGE_TEST_INTEGRATION_DB_ENGINE = "fjall";
|
||||
};
|
||||
|
||||
# lints (fmt, clippy)
|
||||
fmt = lints.garage-cargo-fmt;
|
||||
clippy = lints.garage-cargo-clippy;
|
||||
};
|
||||
|
||||
# ---- developpment shell, for making native builds only ----
|
||||
|
||||
@@ -167,7 +167,7 @@ let
|
||||
</ul></p>
|
||||
<p> Sources:
|
||||
<ul>
|
||||
<li><a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/src/${r.type}/${x.version}">gitea</a></li>
|
||||
<li><a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/src/${r.type}/${x.version}">Forgejo</a></li>
|
||||
<li><a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/archive/${x.version}.zip">.zip</a></li>
|
||||
<li><a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/archive/${x.version}.tar.gz">.tar.gz</a></li>
|
||||
</ul></p>
|
||||
|
||||
@@ -48,7 +48,7 @@ let
|
||||
|
||||
inherit (pkgs) lib stdenv;
|
||||
|
||||
toolchainFn = (p: p.rust-bin.stable."1.82.0".default.override {
|
||||
toolchainFn = (p: p.rust-bin.stable."1.91.0".default.override {
|
||||
targets = lib.optionals (target != null) [ rustTarget ];
|
||||
extensions = [
|
||||
"rust-src"
|
||||
@@ -190,4 +190,15 @@ in rec {
|
||||
pkgs.cacert
|
||||
];
|
||||
} // extraTestEnv);
|
||||
|
||||
# ---- source code linting ----
|
||||
|
||||
garage-cargo-fmt = craneLib.cargoFmt (commonArgs // {
|
||||
cargoExtraArgs = "";
|
||||
});
|
||||
|
||||
garage-cargo-clippy = craneLib.cargoClippy (commonArgs // {
|
||||
cargoArtifacts = garage-deps;
|
||||
cargoClippyExtraArgs = "--all-targets -- -D warnings";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: garage
|
||||
description: S3-compatible object store for small self-hosted geo-distributed deployments
|
||||
type: application
|
||||
version: 0.9.1
|
||||
appVersion: "v2.1.0"
|
||||
version: 0.9.2
|
||||
appVersion: "v2.2.0"
|
||||
home: https://garagehq.deuxfleurs.fr/
|
||||
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# garage
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
S3-compatible object store for small self-hosted geo-distributed deployments
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ If release name contains chart name it will be used as a full name.
|
||||
Create the name of the rpc secret
|
||||
*/}}
|
||||
{{- define "garage.rpcSecretName" -}}
|
||||
{{- printf "%s-rpc-secret" (include "garage.fullname" .) -}}
|
||||
{{- .Values.garage.existingRpcSecret | default (printf "%s-rpc-secret" (include "garage.fullname" .)) -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
@@ -47,8 +47,8 @@ helm.sh/chart: {{ include "garage.chart" . }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{ with .Values.commonLabels }}
|
||||
{{- toYaml . }}
|
||||
{{- with .Values.commonLabels }}
|
||||
{{- toYaml . | nindent 0 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
@@ -28,8 +28,16 @@ data:
|
||||
# rpc_secret will be populated by the init container from a k8s secret object
|
||||
rpc_secret = "__RPC_SECRET_REPLACE__"
|
||||
|
||||
bootstrap_peers = {{ .Values.garage.bootstrapPeers }}
|
||||
bootstrap_peers = [
|
||||
{{- range $index, $peer := .Values.garage.bootstrapPeers }}
|
||||
{{- if $index}}, {{ end }}{{ $peer | quote }}
|
||||
{{ end }}
|
||||
]
|
||||
|
||||
{{- if .Values.garage.additionalTopLevelConfig }}
|
||||
{{ .Values.garage.additionalTopLevelConfig | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
[kubernetes_discovery]
|
||||
namespace = "{{ .Release.Namespace }}"
|
||||
service_name = "{{ include "garage.fullname" . }}"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- if not .Values.garage.existingRpcSecret }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -12,3 +13,4 @@ data:
|
||||
{{- $prevRpcSecret := $prevSecretData.rpcSecret | default "" | b64dec }}
|
||||
{{/* Priority is: 1. from values, 2. previous value, 3. generate random */}}
|
||||
rpcSecret: {{ .Values.garage.rpcSecret | default $prevRpcSecret | default (include "jupyterhub.randHex" 64) | b64enc | quote }}
|
||||
{{- end }}
|
||||
|
||||
@@ -4,6 +4,10 @@ metadata:
|
||||
name: {{ include "garage.fullname" . }}
|
||||
labels:
|
||||
{{- include "garage.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
@@ -37,4 +41,4 @@ spec:
|
||||
name: metrics
|
||||
selector:
|
||||
{{- include "garage.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -37,6 +37,10 @@ garage:
|
||||
rpcBindAddr: "[::]:3901"
|
||||
# -- If not given, a random secret will be generated and stored in a Secret object
|
||||
rpcSecret: ""
|
||||
# -- If you want to provide an rpcSecret within an existing k8s secret,
|
||||
# specify the secret name here, and store the value under the secret key `rpcSecret`
|
||||
# the default secret will not be created
|
||||
existingRpcSecret: ""
|
||||
# -- This is not required if you use the integrated kubernetes discovery
|
||||
bootstrapPeers: []
|
||||
# -- Set to true if you want to use k8s discovery but install the CRDs manually outside
|
||||
@@ -50,6 +54,12 @@ garage:
|
||||
rootDomain: ".web.garage.tld"
|
||||
index: "index.html"
|
||||
|
||||
# -- Additional configuration to append to garage.toml. Use a multi-line string for custom config.
|
||||
# Example:
|
||||
# additionalTopLevelConfig: |-
|
||||
# data_fsync = true
|
||||
additionalTopLevelConfig: ""
|
||||
|
||||
# -- if not empty string, allow using an existing ConfigMap for the garage.toml,
|
||||
# if set, ignores garage.toml
|
||||
existingConfigMap: ""
|
||||
@@ -117,6 +127,7 @@ podSecurityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
runAsNonRoot: true
|
||||
|
||||
securityContext:
|
||||
@@ -133,6 +144,8 @@ service:
|
||||
# - NodePort (+ Ingress)
|
||||
# - LoadBalancer
|
||||
type: ClusterIP
|
||||
# -- Annotations to add to the service
|
||||
annotations: {}
|
||||
s3:
|
||||
api:
|
||||
port: 3900
|
||||
|
||||
@@ -26,7 +26,7 @@ in
|
||||
s3cmd
|
||||
minio-client
|
||||
rclone
|
||||
(python312.withPackages (ps: [ ps.boto3 ]))
|
||||
(python313.withPackages (ps: [ ps.boto3 ]))
|
||||
|
||||
socat
|
||||
psmisc
|
||||
@@ -36,6 +36,8 @@ in
|
||||
jq
|
||||
];
|
||||
shellHook = ''
|
||||
export AWS_REQUEST_CHECKSUM_CALCULATION='when_required'
|
||||
|
||||
function to_s3 {
|
||||
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED \
|
||||
aws \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_api_admin"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -26,7 +26,7 @@ argon2.workspace = true
|
||||
async-trait.workspace = true
|
||||
bytesize.workspace = true
|
||||
chrono.workspace = true
|
||||
err-derive.workspace = true
|
||||
thiserror.workspace = true
|
||||
hex.workspace = true
|
||||
paste.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use err_derive::Error;
|
||||
use hyper::header::HeaderValue;
|
||||
use hyper::{HeaderMap, StatusCode};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use garage_model::helper::error::Error as HelperError;
|
||||
|
||||
@@ -16,36 +16,33 @@ use garage_api_common::helpers::*;
|
||||
/// Errors of this crate
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(display = "{}", _0)]
|
||||
#[error("{0}")]
|
||||
/// Error from common error
|
||||
Common(#[error(source)] CommonError),
|
||||
Common(#[from] CommonError),
|
||||
|
||||
// Category: cannot process
|
||||
/// The admin API token does not exist
|
||||
#[error(display = "Admin token not found: {}", _0)]
|
||||
#[error("Admin token not found: {0}")]
|
||||
NoSuchAdminToken(String),
|
||||
|
||||
/// The API access key does not exist
|
||||
#[error(display = "Access key not found: {}", _0)]
|
||||
#[error("Access key not found: {0}")]
|
||||
NoSuchAccessKey(String),
|
||||
|
||||
/// The requested block does not exist
|
||||
#[error(display = "Block not found: {}", _0)]
|
||||
#[error("Block not found: {0}")]
|
||||
NoSuchBlock(String),
|
||||
|
||||
/// The requested worker does not exist
|
||||
#[error(display = "Worker not found: {}", _0)]
|
||||
#[error("Worker not found: {0}")]
|
||||
NoSuchWorker(u64),
|
||||
|
||||
/// The object requested don't exists
|
||||
#[error(display = "Key not found")]
|
||||
#[error("Key not found")]
|
||||
NoSuchKey,
|
||||
|
||||
/// In Import key, the key already exists
|
||||
#[error(
|
||||
display = "Key {} already exists in data store. Even if it is deleted, we can't let you create a new key with the same ID. Sorry.",
|
||||
_0
|
||||
)]
|
||||
#[error("Key {0} already exists in data store. Even if it is deleted, we can't let you create a new key with the same ID. Sorry.")]
|
||||
KeyAlreadyExists(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use utoipa::{Modify, OpenApi};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{Modify, OpenApi, ToSchema};
|
||||
|
||||
use crate::api::*;
|
||||
|
||||
@@ -246,7 +247,7 @@ For example to declare 100GB, you must set `capacity: 100000000000`.
|
||||
Garage uses internally the International System of Units (SI), it assumes that 1kB = 1000 bytes, and displays storage as kB, MB, GB (and not KiB, MiB, GiB that assume 1KiB = 1024 bytes).
|
||||
",
|
||||
request_body(
|
||||
content=UpdateClusterLayoutRequest,
|
||||
content=UpdateClusterLayoutRequestOpenapi,
|
||||
description="
|
||||
To add a new node to the layout or to change the configuration of an existing node, simply set the values you want (`zone`, `capacity`, and `tags`).
|
||||
To remove a node, simply pass the `remove: true` field.
|
||||
@@ -262,6 +263,48 @@ Contrary to the CLI that may update only a subset of the fields capacity, zone a
|
||||
)]
|
||||
fn UpdateClusterLayout() -> () {}
|
||||
|
||||
// Hack: we cannot use the UpdateClusterLayoutRequest from api.rs,
|
||||
// as it contains (via NodeRoleChange) an untagged enum flattenned into
|
||||
// a struct, which breaks the openapi generator.
|
||||
// See issue #1249.
|
||||
// Instead, we use a rewritten version of the NodeRoleChange struct where
|
||||
// the struct fields are distributed into the enum variants (this is an equivalent
|
||||
// representation, but this way we avoid having to rewrite all uses of the original
|
||||
// struct in the Garage codebase).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[schema(as = UpdateClusterLayoutRequest)]
|
||||
pub struct UpdateClusterLayoutRequestOpenapi {
|
||||
/// New node roles to assign or remove in the cluster layout
|
||||
#[serde(default)]
|
||||
pub roles: Vec<NodeRoleChangeOpenapi>,
|
||||
/// New layout computation parameters to use
|
||||
#[serde(default)]
|
||||
pub parameters: Option<LayoutParameters>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[schema(as = NodeRoleChangeRequest)]
|
||||
#[serde(untagged)]
|
||||
pub enum NodeRoleChangeOpenapi {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Remove {
|
||||
/// ID of the node for which this change applies
|
||||
id: String,
|
||||
/// Set `remove` to `true` to remove the node from the layout
|
||||
remove: bool,
|
||||
},
|
||||
Update(NodeRoleUpdate),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NodeRoleUpdate {
|
||||
/// ID of the node for which this change applies
|
||||
id: String,
|
||||
#[serde(flatten)]
|
||||
role: NodeAssignedRole,
|
||||
}
|
||||
|
||||
#[utoipa::path(post,
|
||||
path = "/v2/PreviewClusterLayoutChanges",
|
||||
tag = "Cluster layout",
|
||||
@@ -586,7 +629,7 @@ fn DenyBucketKey() -> () {}
|
||||
path = "/v2/AddBucketAlias",
|
||||
tag = "Bucket alias",
|
||||
description = "Add an alias for the target bucket. This can be either a global or a local alias, depending on which fields are specified.",
|
||||
request_body = AddBucketAliasRequest,
|
||||
request_body = BucketAliasEnumOpenapi,
|
||||
responses(
|
||||
(status = 200, description = "Returns exhaustive information about the bucket", body = AddBucketAliasResponse),
|
||||
(status = 500, description = "Internal server error")
|
||||
@@ -598,7 +641,7 @@ fn AddBucketAlias() -> () {}
|
||||
path = "/v2/RemoveBucketAlias",
|
||||
tag = "Bucket alias",
|
||||
description = "Remove an alias for the target bucket. This can be either a global or a local alias, depending on which fields are specified.",
|
||||
request_body = RemoveBucketAliasRequest,
|
||||
request_body = BucketAliasEnumOpenapi,
|
||||
responses(
|
||||
(status = 200, description = "Returns exhaustive information about the bucket", body = RemoveBucketAliasResponse),
|
||||
(status = 500, description = "Internal server error")
|
||||
@@ -606,6 +649,24 @@ fn AddBucketAlias() -> () {}
|
||||
)]
|
||||
fn RemoveBucketAlias() -> () {}
|
||||
|
||||
// Hack for issue #1249 (see UpdateClusterLayout)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(untagged)]
|
||||
#[schema(as = BucketAliasEnum)]
|
||||
pub enum BucketAliasEnumOpenapi {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Global {
|
||||
bucket_id: String,
|
||||
global_alias: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Local {
|
||||
bucket_id: String,
|
||||
local_alias: String,
|
||||
access_key_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
// **********************************************
|
||||
// Node operations
|
||||
// **********************************************
|
||||
@@ -818,7 +879,7 @@ impl Modify for SecurityAddon {
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(
|
||||
version = "v2.1.0",
|
||||
version = "v2.2.0",
|
||||
title = "Garage administration API",
|
||||
description = "Administrate your Garage cluster programatically, including status, layout, keys, buckets, and maintainance tasks.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_api_common"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -23,7 +23,7 @@ bytes.workspace = true
|
||||
chrono.workspace = true
|
||||
crc-fast.workspace = true
|
||||
crypto-common.workspace = true
|
||||
err-derive.workspace = true
|
||||
thiserror.workspace = true
|
||||
hex.workspace = true
|
||||
hmac.workspace = true
|
||||
md-5.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use err_derive::Error;
|
||||
use hyper::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
use garage_util::error::Error as GarageError;
|
||||
|
||||
@@ -12,48 +12,48 @@ use garage_model::helper::error::Error as HelperError;
|
||||
pub enum CommonError {
|
||||
// ---- INTERNAL ERRORS ----
|
||||
/// Error related to deeper parts of Garage
|
||||
#[error(display = "Internal error: {}", _0)]
|
||||
InternalError(#[error(source)] GarageError),
|
||||
#[error("Internal error: {0}")]
|
||||
InternalError(#[from] GarageError),
|
||||
|
||||
/// Error related to Hyper
|
||||
#[error(display = "Internal error (Hyper error): {}", _0)]
|
||||
Hyper(#[error(source)] hyper::Error),
|
||||
#[error("Internal error (Hyper error): {0}")]
|
||||
Hyper(#[from] hyper::Error),
|
||||
|
||||
/// Error related to HTTP
|
||||
#[error(display = "Internal error (HTTP error): {}", _0)]
|
||||
Http(#[error(source)] http::Error),
|
||||
#[error("Internal error (HTTP error): {0}")]
|
||||
Http(#[from] http::Error),
|
||||
|
||||
// ---- GENERIC CLIENT ERRORS ----
|
||||
/// Proper authentication was not provided
|
||||
#[error(display = "Forbidden: {}", _0)]
|
||||
#[error("Forbidden: {0}")]
|
||||
Forbidden(String),
|
||||
|
||||
/// Generic bad request response with custom message
|
||||
#[error(display = "Bad request: {}", _0)]
|
||||
#[error("Bad request: {0}")]
|
||||
BadRequest(String),
|
||||
|
||||
/// The client sent a header with invalid value
|
||||
#[error(display = "Invalid header value: {}", _0)]
|
||||
InvalidHeader(#[error(source)] hyper::header::ToStrError),
|
||||
#[error("Invalid header value: {0}")]
|
||||
InvalidHeader(#[from] hyper::header::ToStrError),
|
||||
|
||||
// ---- SPECIFIC ERROR CONDITIONS ----
|
||||
// These have to be error codes referenced in the S3 spec here:
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
|
||||
/// The bucket requested don't exists
|
||||
#[error(display = "Bucket not found: {}", _0)]
|
||||
#[error("Bucket not found: {0}")]
|
||||
NoSuchBucket(String),
|
||||
|
||||
/// Tried to create a bucket that already exist
|
||||
#[error(display = "Bucket already exists")]
|
||||
#[error("Bucket already exists")]
|
||||
BucketAlreadyExists,
|
||||
|
||||
/// Tried to delete a non-empty bucket
|
||||
#[error(display = "Tried to delete a non-empty bucket")]
|
||||
#[error("Tried to delete a non-empty bucket")]
|
||||
BucketNotEmpty,
|
||||
|
||||
// Category: bad request
|
||||
/// Bucket name is not valid according to AWS S3 specs
|
||||
#[error(display = "Invalid bucket name: {}", _0)]
|
||||
#[error("Invalid bucket name: {0}")]
|
||||
InvalidBucketName(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use err_derive::Error;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::common_error::CommonError;
|
||||
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
||||
@@ -6,21 +6,21 @@ pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInterna
|
||||
/// Errors of this crate
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(display = "{}", _0)]
|
||||
#[error("{0}")]
|
||||
/// Error from common error
|
||||
Common(CommonError),
|
||||
|
||||
/// Authorization Header Malformed
|
||||
#[error(display = "Authorization header malformed, unexpected scope: {}", _0)]
|
||||
#[error("Authorization header malformed, unexpected scope: {0}")]
|
||||
AuthorizationHeaderMalformed(String),
|
||||
|
||||
// Category: bad request
|
||||
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
|
||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
||||
InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
|
||||
#[error("Invalid UTF-8: {0}")]
|
||||
InvalidUtf8Str(#[from] std::str::Utf8Error),
|
||||
|
||||
/// The provided digest (checksum) value was invalid
|
||||
#[error(display = "Invalid digest: {}", _0)]
|
||||
#[error("Invalid digest: {0}")]
|
||||
InvalidDigest(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ fn check_standard_signature(
|
||||
// Verify that all necessary request headers are included in signed_headers
|
||||
// The following must be included for all signatures:
|
||||
// - the Host header (mandatory)
|
||||
// - all x-amz-* headers used in the request
|
||||
// - all x-amz-* headers used in the request (except x-amz-content-sha256)
|
||||
// AWS also indicates that the Content-Type header should be signed if
|
||||
// it is used, but Minio client doesn't sign it so we don't check it for compatibility.
|
||||
let signed_headers = split_signed_headers(&authorization)?;
|
||||
@@ -152,7 +152,7 @@ fn check_presigned_signature(
|
||||
// Verify that all necessary request headers are included in signed_headers
|
||||
// For AWSv4 pre-signed URLs, the following must be included:
|
||||
// - the Host header (mandatory)
|
||||
// - all x-amz-* headers used in the request
|
||||
// - all x-amz-* headers used in the request (except x-amz-content-sha256)
|
||||
let signed_headers = split_signed_headers(&authorization)?;
|
||||
verify_signed_headers(request.headers(), &signed_headers)?;
|
||||
|
||||
@@ -269,7 +269,9 @@ fn verify_signed_headers(headers: &HeaderMap, signed_headers: &[HeaderName]) ->
|
||||
return Err(Error::bad_request("Header `Host` should be signed"));
|
||||
}
|
||||
for (name, _) in headers.iter() {
|
||||
if name.as_str().starts_with("x-amz-") {
|
||||
// Enforce signature of all x-amz-* headers, except x-amz-content-sh256
|
||||
// because it is included in the canonical request in all cases
|
||||
if name.as_str().starts_with("x-amz-") && name != X_AMZ_CONTENT_SHA256 {
|
||||
if !signed_headers.contains(name) {
|
||||
return Err(Error::bad_request(format!(
|
||||
"Header `{}` should be signed",
|
||||
@@ -475,8 +477,7 @@ impl Authorization {
|
||||
|
||||
let date = headers
|
||||
.get(X_AMZ_DATE)
|
||||
.ok_or_bad_request("Missing X-Amz-Date field")
|
||||
.map_err(Error::from)?
|
||||
.ok_or_bad_request("Missing X-Amz-Date field")?
|
||||
.to_str()?;
|
||||
let date = parse_date(date)?;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_api_k2v"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -20,7 +20,7 @@ garage_util = { workspace = true, features = [ "k2v" ] }
|
||||
garage_api_common.workspace = true
|
||||
|
||||
base64.workspace = true
|
||||
err-derive.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
futures.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use err_derive::Error;
|
||||
use hyper::header::HeaderValue;
|
||||
use hyper::{HeaderMap, StatusCode};
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) use garage_api_common::common_error::pass_helper_error;
|
||||
use garage_api_common::common_error::{commonErrorDerivative, CommonError};
|
||||
@@ -14,38 +14,38 @@ use garage_api_common::signature::error::Error as SignatureError;
|
||||
/// Errors of this crate
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(display = "{}", _0)]
|
||||
#[error("{0}")]
|
||||
/// Error from common error
|
||||
Common(#[error(source)] CommonError),
|
||||
Common(#[from] CommonError),
|
||||
|
||||
// Category: cannot process
|
||||
/// Authorization Header Malformed
|
||||
#[error(display = "Authorization header malformed, unexpected scope: {}", _0)]
|
||||
#[error("Authorization header malformed, unexpected scope: {0}")]
|
||||
AuthorizationHeaderMalformed(String),
|
||||
|
||||
/// The provided digest (checksum) value was invalid
|
||||
#[error(display = "Invalid digest: {}", _0)]
|
||||
#[error("Invalid digest: {0}")]
|
||||
InvalidDigest(String),
|
||||
|
||||
/// The object requested don't exists
|
||||
#[error(display = "Key not found")]
|
||||
#[error("Key not found")]
|
||||
NoSuchKey,
|
||||
|
||||
/// Some base64 encoded data was badly encoded
|
||||
#[error(display = "Invalid base64: {}", _0)]
|
||||
InvalidBase64(#[error(source)] base64::DecodeError),
|
||||
#[error("Invalid base64: {0}")]
|
||||
InvalidBase64(#[from] base64::DecodeError),
|
||||
|
||||
/// Invalid causality token
|
||||
#[error(display = "Invalid causality token")]
|
||||
#[error("Invalid causality token")]
|
||||
InvalidCausalityToken,
|
||||
|
||||
/// The client asked for an invalid return format (invalid Accept header)
|
||||
#[error(display = "Not acceptable: {}", _0)]
|
||||
#[error("Not acceptable: {0}")]
|
||||
NotAcceptable(String),
|
||||
|
||||
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
|
||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
||||
InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
|
||||
#[error("Invalid UTF-8: {0}")]
|
||||
InvalidUtf8Str(#[from] std::str::Utf8Error),
|
||||
}
|
||||
|
||||
commonErrorDerivative!(Error);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_api_s3"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -28,7 +28,7 @@ base64.workspace = true
|
||||
bytes.workspace = true
|
||||
chrono.workspace = true
|
||||
crc-fast.workspace = true
|
||||
err-derive.workspace = true
|
||||
thiserror.workspace = true
|
||||
hex.workspace = true
|
||||
hmac.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::convert::TryInto;
|
||||
|
||||
use err_derive::Error;
|
||||
use hyper::header::HeaderValue;
|
||||
use hyper::{HeaderMap, StatusCode};
|
||||
use thiserror::Error;
|
||||
|
||||
use garage_model::helper::error::Error as HelperError;
|
||||
|
||||
@@ -25,67 +25,67 @@ use crate::xml as s3_xml;
|
||||
/// Errors of this crate
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(display = "{}", _0)]
|
||||
#[error("{0}")]
|
||||
/// Error from common error
|
||||
Common(#[error(source)] CommonError),
|
||||
Common(#[from] CommonError),
|
||||
|
||||
// Category: cannot process
|
||||
/// Authorization Header Malformed
|
||||
#[error(display = "Authorization header malformed, unexpected scope: {}", _0)]
|
||||
#[error("Authorization header malformed, unexpected scope: {0}")]
|
||||
AuthorizationHeaderMalformed(String),
|
||||
|
||||
/// The object requested don't exists
|
||||
#[error(display = "Key not found")]
|
||||
#[error("Key not found")]
|
||||
NoSuchKey,
|
||||
|
||||
/// The multipart upload requested don't exists
|
||||
#[error(display = "Upload not found")]
|
||||
#[error("Upload not found")]
|
||||
NoSuchUpload,
|
||||
|
||||
/// Precondition failed (e.g. x-amz-copy-source-if-match)
|
||||
#[error(display = "At least one of the preconditions you specified did not hold")]
|
||||
#[error("At least one of the preconditions you specified did not hold")]
|
||||
PreconditionFailed,
|
||||
|
||||
/// Parts specified in CMU request do not match parts actually uploaded
|
||||
#[error(display = "Parts given to CompleteMultipartUpload do not match uploaded parts")]
|
||||
#[error("Parts given to CompleteMultipartUpload do not match uploaded parts")]
|
||||
InvalidPart,
|
||||
|
||||
/// Parts given to CompleteMultipartUpload were not in ascending order
|
||||
#[error(display = "Parts given to CompleteMultipartUpload were not in ascending order")]
|
||||
#[error("Parts given to CompleteMultipartUpload were not in ascending order")]
|
||||
InvalidPartOrder,
|
||||
|
||||
/// In CompleteMultipartUpload: not enough data
|
||||
/// (here we are more lenient than AWS S3)
|
||||
#[error(display = "Proposed upload is smaller than the minimum allowed object size")]
|
||||
#[error("Proposed upload is smaller than the minimum allowed object size")]
|
||||
EntityTooSmall,
|
||||
|
||||
// Category: bad request
|
||||
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
|
||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
||||
InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
|
||||
#[error("Invalid UTF-8: {0}")]
|
||||
InvalidUtf8Str(#[from] std::str::Utf8Error),
|
||||
|
||||
/// The request used an invalid path
|
||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
||||
InvalidUtf8String(#[error(source)] std::string::FromUtf8Error),
|
||||
#[error("Invalid UTF-8: {0}")]
|
||||
InvalidUtf8String(#[from] std::string::FromUtf8Error),
|
||||
|
||||
/// The client sent invalid XML data
|
||||
#[error(display = "Invalid XML: {}", _0)]
|
||||
#[error("Invalid XML: {0}")]
|
||||
InvalidXml(String),
|
||||
|
||||
/// The client sent a range header with invalid value
|
||||
#[error(display = "Invalid HTTP range: {:?}", _0)]
|
||||
InvalidRange(#[error(from)] (http_range::HttpRangeParseError, u64)),
|
||||
#[error("Invalid HTTP range: {0:?}")]
|
||||
InvalidRange((http_range::HttpRangeParseError, u64)),
|
||||
|
||||
/// The client sent a range header with invalid value
|
||||
#[error(display = "Invalid encryption algorithm: {:?}, should be AES256", _0)]
|
||||
#[error("Invalid encryption algorithm: {0:?}, should be AES256")]
|
||||
InvalidEncryptionAlgorithm(String),
|
||||
|
||||
/// The provided digest (checksum) value was invalid
|
||||
#[error(display = "Invalid digest: {}", _0)]
|
||||
#[error("Invalid digest: {0}")]
|
||||
InvalidDigest(String),
|
||||
|
||||
/// The client sent a request for an action not supported by garage
|
||||
#[error(display = "Unimplemented action: {}", _0)]
|
||||
#[error("Unimplemented action: {0}")]
|
||||
NotImplemented(String),
|
||||
}
|
||||
|
||||
@@ -99,6 +99,12 @@ impl From<HelperError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(http_range::HttpRangeParseError, u64)> for Error {
|
||||
fn from(err: (http_range::HttpRangeParseError, u64)) -> Error {
|
||||
Error::InvalidRange(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<roxmltree::Error> for Error {
|
||||
fn from(err: roxmltree::Error) -> Self {
|
||||
Self::InvalidXml(format!("{}", err))
|
||||
|
||||
@@ -853,7 +853,9 @@ impl PreconditionHeaders {
|
||||
}
|
||||
|
||||
fn check(&self, v: &ObjectVersion, etag: &str) -> Result<Option<StatusCode>, Error> {
|
||||
let v_date = UNIX_EPOCH + Duration::from_millis(v.timestamp);
|
||||
// we store date with ms precision, but headers are precise to the second: truncate
|
||||
// the timestamp to handle the same-second edge case
|
||||
let v_date = UNIX_EPOCH + Duration::from_secs(v.timestamp / 1000);
|
||||
|
||||
// Implemented from https://datatracker.ietf.org/doc/html/rfc7232#section-6
|
||||
|
||||
|
||||
@@ -138,10 +138,26 @@ pub async fn handle_post_object(
|
||||
|
||||
let mut conditions = decoded_policy.into_conditions()?;
|
||||
|
||||
// If there are conditions on the bucket name, check these against the actual bucket_name rather
|
||||
// than the one in params, which is allowed to be absent.
|
||||
if let Some(conds) = conditions.params.remove("bucket") {
|
||||
for cond in conds {
|
||||
let ok = match cond {
|
||||
Operation::Equal(s) => s.as_str() == bucket_name,
|
||||
Operation::StartsWith(s) => bucket_name.starts_with(&s),
|
||||
};
|
||||
if !ok {
|
||||
return Err(Error::bad_request(
|
||||
"Key 'bucket' has value not allowed in policy",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (param_key, value) in params.iter() {
|
||||
let param_key = param_key.as_str();
|
||||
match param_key {
|
||||
"policy" | "x-amz-signature" => (), // this is always accepted, as it's required to validate other fields
|
||||
"policy" | "x-amz-signature" | "bucket" => (), // this is always accepted, as it's required to validate other fields
|
||||
"content-type" => {
|
||||
let conds = conditions.params.remove("content-type").ok_or_else(|| {
|
||||
Error::bad_request(format!("Key '{}' is not allowed in policy", param_key))
|
||||
|
||||
@@ -39,8 +39,6 @@ use crate::encryption::{EncryptionParams, OekDerivationInfo};
|
||||
use crate::error::*;
|
||||
use crate::website::X_AMZ_WEBSITE_REDIRECT_LOCATION;
|
||||
|
||||
const PUT_BLOCKS_MAX_PARALLEL: usize = 3;
|
||||
|
||||
pub(crate) struct SaveStreamResult {
|
||||
pub(crate) version_uuid: Uuid,
|
||||
pub(crate) version_timestamp: u64,
|
||||
@@ -507,7 +505,7 @@ pub(crate) async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> +
|
||||
};
|
||||
let recv_next = async {
|
||||
// If more than a maximum number of writes are in progress, don't add more for now
|
||||
if currently_running >= PUT_BLOCKS_MAX_PARALLEL {
|
||||
if currently_running >= ctx.garage.config.block_max_concurrent_writes_per_request {
|
||||
futures::future::pending().await
|
||||
} else {
|
||||
block_rx3.recv().await
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_block"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_db"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -12,7 +12,7 @@ readme = "../../README.md"
|
||||
path = "lib.rs"
|
||||
|
||||
[dependencies]
|
||||
err-derive.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
heed = { workspace = true, optional = true }
|
||||
|
||||
@@ -20,7 +20,7 @@ use std::cell::Cell;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use err_derive::Error;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use open::*;
|
||||
|
||||
@@ -44,7 +44,7 @@ pub type TxValueIter<'a> = Box<dyn std::iter::Iterator<Item = TxOpResult<(Value,
|
||||
// ----
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error(display = "{}", _0)]
|
||||
#[error("{0}")]
|
||||
pub struct Error(pub Cow<'static, str>);
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
@@ -56,7 +56,7 @@ impl From<std::io::Error> for Error {
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error(display = "{}", _0)]
|
||||
#[error("{0}")]
|
||||
pub struct TxOpError(pub(crate) Error);
|
||||
pub type TxOpResult<T> = std::result::Result<T, TxOpError>;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
@@ -68,24 +68,30 @@ struct Opt {
|
||||
async fn main() {
|
||||
// Initialize version and features info
|
||||
let features = &[
|
||||
#[cfg(feature = "k2v")]
|
||||
"k2v",
|
||||
#[cfg(feature = "lmdb")]
|
||||
"lmdb",
|
||||
#[cfg(feature = "sqlite")]
|
||||
"sqlite",
|
||||
#[cfg(feature = "consul-discovery")]
|
||||
"consul-discovery",
|
||||
#[cfg(feature = "kubernetes-discovery")]
|
||||
"kubernetes-discovery",
|
||||
#[cfg(feature = "metrics")]
|
||||
"metrics",
|
||||
#[cfg(feature = "telemetry-otlp")]
|
||||
"telemetry-otlp",
|
||||
#[cfg(feature = "bundled-libs")]
|
||||
"bundled-libs",
|
||||
#[cfg(feature = "consul-discovery")]
|
||||
"consul-discovery",
|
||||
#[cfg(feature = "fjall")]
|
||||
"fjall",
|
||||
#[cfg(feature = "journald")]
|
||||
"journald",
|
||||
#[cfg(feature = "k2v")]
|
||||
"k2v",
|
||||
#[cfg(feature = "kubernetes-discovery")]
|
||||
"kubernetes-discovery",
|
||||
#[cfg(feature = "lmdb")]
|
||||
"lmdb",
|
||||
#[cfg(feature = "metrics")]
|
||||
"metrics",
|
||||
#[cfg(feature = "sqlite")]
|
||||
"sqlite",
|
||||
#[cfg(feature = "syslog")]
|
||||
"syslog",
|
||||
#[cfg(feature = "system-libs")]
|
||||
"system-libs",
|
||||
#[cfg(feature = "telemetry-otlp")]
|
||||
"telemetry-otlp",
|
||||
][..];
|
||||
if let Some(git_version) = option_env!("GIT_VERSION") {
|
||||
garage_util::version::init_version(git_version);
|
||||
|
||||
@@ -198,6 +198,7 @@ async fn test_precondition() {
|
||||
);
|
||||
}
|
||||
let older_date = DateTime::from_secs_f64(last_modified.as_secs_f64() - 10.0);
|
||||
let same_date = DateTime::from_secs_f64(last_modified.as_secs_f64());
|
||||
let newer_date = DateTime::from_secs_f64(last_modified.as_secs_f64() + 10.0);
|
||||
{
|
||||
let err = ctx
|
||||
@@ -212,6 +213,18 @@ async fn test_precondition() {
|
||||
matches!(err, Err(SdkError::ServiceError(se)) if se.raw().status().as_u16() == 304)
|
||||
);
|
||||
|
||||
let err = ctx
|
||||
.client
|
||||
.get_object()
|
||||
.bucket(&bucket)
|
||||
.key(STD_KEY)
|
||||
.if_modified_since(same_date)
|
||||
.send()
|
||||
.await;
|
||||
assert!(
|
||||
matches!(err, Err(SdkError::ServiceError(se)) if se.raw().status().as_u16() == 304)
|
||||
);
|
||||
|
||||
let o = ctx
|
||||
.client
|
||||
.get_object()
|
||||
@@ -236,6 +249,17 @@ async fn test_precondition() {
|
||||
matches!(err, Err(SdkError::ServiceError(se)) if se.raw().status().as_u16() == 412)
|
||||
);
|
||||
|
||||
let o = ctx
|
||||
.client
|
||||
.get_object()
|
||||
.bucket(&bucket)
|
||||
.key(STD_KEY)
|
||||
.if_unmodified_since(same_date)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(o.e_tag.as_ref().unwrap().as_str(), etag);
|
||||
|
||||
let o = ctx
|
||||
.client
|
||||
.get_object()
|
||||
|
||||
@@ -573,24 +573,68 @@ async fn test_website_redirect_full_bucket() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("http://127.0.0.1:{}/my-path", ctx.garage.web_port))
|
||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||
.body(Body::new(Bytes::new()))
|
||||
.unwrap();
|
||||
{
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("http://127.0.0.1:{}/my-path", ctx.garage.web_port))
|
||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||
.body(Body::new(Bytes::new()))
|
||||
.unwrap();
|
||||
|
||||
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||
let resp = client.request(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
assert_eq!(
|
||||
resp.headers()
|
||||
.get(hyper::header::LOCATION)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"https://other.tld/my-path"
|
||||
);
|
||||
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||
let resp = client.request(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
assert_eq!(
|
||||
resp.headers()
|
||||
.get(hyper::header::LOCATION)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"https://other.tld/my-path"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("http://127.0.0.1:{}/my-path/", ctx.garage.web_port))
|
||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||
.body(Body::new(Bytes::new()))
|
||||
.unwrap();
|
||||
|
||||
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||
let resp = client.request(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
assert_eq!(
|
||||
resp.headers()
|
||||
.get(hyper::header::LOCATION)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"https://other.tld/my-path/"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("http://127.0.0.1:{}/", ctx.garage.web_port))
|
||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||
.body(Body::new(Bytes::new()))
|
||||
.unwrap();
|
||||
|
||||
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||
let resp = client.request(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
assert_eq!(
|
||||
resp.headers()
|
||||
.get(hyper::header::LOCATION)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"https://other.tld/"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_model"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -25,7 +25,7 @@ argon2.workspace = true
|
||||
async-trait.workspace = true
|
||||
blake2.workspace = true
|
||||
chrono.workspace = true
|
||||
err-derive.workspace = true
|
||||
thiserror.workspace = true
|
||||
hex.workspace = true
|
||||
http.workspace = true
|
||||
base64.workspace = true
|
||||
|
||||
@@ -321,15 +321,15 @@ impl Garage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn bucket_helper(&self) -> helper::bucket::BucketHelper {
|
||||
pub fn bucket_helper(&self) -> helper::bucket::BucketHelper<'_> {
|
||||
helper::bucket::BucketHelper(self)
|
||||
}
|
||||
|
||||
pub fn key_helper(&self) -> helper::key::KeyHelper {
|
||||
pub fn key_helper(&self) -> helper::key::KeyHelper<'_> {
|
||||
helper::key::KeyHelper(self)
|
||||
}
|
||||
|
||||
pub async fn locked_helper(&self) -> helper::locked::LockedHelper {
|
||||
pub async fn locked_helper(&self) -> helper::locked::LockedHelper<'_> {
|
||||
let lock = self.bucket_lock.lock().await;
|
||||
helper::locked::LockedHelper(self, Some(lock))
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
use err_derive::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use garage_util::error::Error as GarageError;
|
||||
|
||||
#[derive(Debug, Error, Serialize, Deserialize)]
|
||||
pub enum Error {
|
||||
#[error(display = "Internal error: {}", _0)]
|
||||
Internal(#[error(source)] GarageError),
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(#[from] GarageError),
|
||||
|
||||
#[error(display = "Bad request: {}", _0)]
|
||||
#[error("Bad request: {0}")]
|
||||
BadRequest(String),
|
||||
|
||||
/// Bucket name is not valid according to AWS S3 specs
|
||||
#[error(display = "Invalid bucket name: {}", _0)]
|
||||
#[error("Invalid bucket name: {0}")]
|
||||
InvalidBucketName(String),
|
||||
|
||||
#[error(display = "Access key not found: {}", _0)]
|
||||
#[error("Access key not found: {0}")]
|
||||
NoSuchAccessKey(String),
|
||||
|
||||
#[error(display = "Bucket not found: {}", _0)]
|
||||
#[error("Bucket not found: {0}")]
|
||||
NoSuchBucket(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_net"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -30,7 +30,7 @@ rand.workspace = true
|
||||
|
||||
log.workspace = true
|
||||
arc-swap.workspace = true
|
||||
err-derive.workspace = true
|
||||
thiserror.workspace = true
|
||||
bytes.workspace = true
|
||||
cfg-if.workspace = true
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ where
|
||||
pub(crate) type DynEndpoint = Box<dyn GenericEndpoint + Send + Sync>;
|
||||
|
||||
pub(crate) trait GenericEndpoint {
|
||||
fn handle(&self, req_enc: ReqEnc, from: NodeID) -> BoxFuture<Result<RespEnc, Error>>;
|
||||
fn handle(&self, req_enc: ReqEnc, from: NodeID) -> BoxFuture<'_, Result<RespEnc, Error>>;
|
||||
fn drop_handler(&self);
|
||||
fn clone_endpoint(&self) -> DynEndpoint;
|
||||
}
|
||||
@@ -175,7 +175,7 @@ where
|
||||
M: Message,
|
||||
H: StreamingEndpointHandler<M> + 'static,
|
||||
{
|
||||
fn handle(&self, req_enc: ReqEnc, from: NodeID) -> BoxFuture<Result<RespEnc, Error>> {
|
||||
fn handle(&self, req_enc: ReqEnc, from: NodeID) -> BoxFuture<'_, Result<RespEnc, Error>> {
|
||||
async move {
|
||||
match self.0.handler.load_full() {
|
||||
None => Err(Error::NoHandler),
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
use std::io;
|
||||
|
||||
use err_derive::Error;
|
||||
use log::error;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(display = "IO error: {}", _0)]
|
||||
Io(#[error(source)] io::Error),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error(display = "Messagepack encode error: {}", _0)]
|
||||
RMPEncode(#[error(source)] rmp_serde::encode::Error),
|
||||
#[error(display = "Messagepack decode error: {}", _0)]
|
||||
RMPDecode(#[error(source)] rmp_serde::decode::Error),
|
||||
#[error("Messagepack encode error: {0}")]
|
||||
RMPEncode(#[from] rmp_serde::encode::Error),
|
||||
#[error("Messagepack decode error: {0}")]
|
||||
RMPDecode(#[from] rmp_serde::decode::Error),
|
||||
|
||||
#[error(display = "Tokio join error: {}", _0)]
|
||||
TokioJoin(#[error(source)] tokio::task::JoinError),
|
||||
#[error("Tokio join error: {0}")]
|
||||
TokioJoin(#[from] tokio::task::JoinError),
|
||||
|
||||
#[error(display = "oneshot receive error: {}", _0)]
|
||||
OneshotRecv(#[error(source)] tokio::sync::oneshot::error::RecvError),
|
||||
#[error("oneshot receive error: {0}")]
|
||||
OneshotRecv(#[from] tokio::sync::oneshot::error::RecvError),
|
||||
|
||||
#[error(display = "Handshake error: {}", _0)]
|
||||
Handshake(#[error(source)] kuska_handshake::async_std::Error),
|
||||
#[error("Handshake error: {0}")]
|
||||
Handshake(#[from] kuska_handshake::async_std::Error),
|
||||
|
||||
#[error(display = "UTF8 error: {}", _0)]
|
||||
UTF8(#[error(source)] std::string::FromUtf8Error),
|
||||
#[error("UTF8 error: {0}")]
|
||||
UTF8(#[from] std::string::FromUtf8Error),
|
||||
|
||||
#[error(display = "Framing protocol error")]
|
||||
#[error("Framing protocol error")]
|
||||
Framing,
|
||||
|
||||
#[error(display = "Remote error ({:?}): {}", _0, _1)]
|
||||
#[error("Remote error ({0:?}): {1}")]
|
||||
Remote(io::ErrorKind, String),
|
||||
|
||||
#[error(display = "Request ID collision")]
|
||||
#[error("Request ID collision")]
|
||||
IdCollision,
|
||||
|
||||
#[error(display = "{}", _0)]
|
||||
#[error("{0}")]
|
||||
Message(String),
|
||||
|
||||
#[error(display = "No handler / shutting down")]
|
||||
#[error("No handler / shutting down")]
|
||||
NoHandler,
|
||||
|
||||
#[error(display = "Connection closed")]
|
||||
#[error("Connection closed")]
|
||||
ConnectionClosed,
|
||||
|
||||
#[error(display = "Version mismatch: {}", _0)]
|
||||
#[error("Version mismatch: {0}")]
|
||||
VersionMismatch(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_rpc"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -33,7 +33,7 @@ async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
serde_bytes.workspace = true
|
||||
serde_json.workspace = true
|
||||
err-derive = { workspace = true, optional = true }
|
||||
thiserror = { workspace = true, optional = true }
|
||||
|
||||
# newer version requires rust edition 2021
|
||||
kube = { workspace = true, optional = true }
|
||||
@@ -49,5 +49,5 @@ opentelemetry.workspace = true
|
||||
|
||||
[features]
|
||||
kubernetes-discovery = [ "kube", "k8s-openapi", "schemars" ]
|
||||
consul-discovery = [ "reqwest", "err-derive" ]
|
||||
consul-discovery = [ "reqwest", "thiserror" ]
|
||||
system-libs = [ "sodiumoxide/use-pkg-config" ]
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
use err_derive::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use garage_net::NodeID;
|
||||
|
||||
@@ -126,38 +126,61 @@ impl ConsulDiscovery {
|
||||
}
|
||||
|
||||
// ---- READING FROM CONSUL CATALOG ----
|
||||
|
||||
/// Query Consul for Garage nodes registered under the configured service name.
|
||||
///
|
||||
/// This method supports querying multiple Consul datacenters for WAN or
|
||||
/// multi-datacenter deployments. If `config.datacenters` is set and non-empty,
|
||||
/// each listed datacenter is queried and the results are aggregated. Otherwise,
|
||||
/// only the local datacenter is queried. `config.datacenters` does not need to be set
|
||||
/// when all the datacenters are on the same LAN, in this case service discovery works normally
|
||||
///
|
||||
/// # Returns
|
||||
/// A list of `(NodeID, SocketAddr)` pairs corresponding to all valid discovered
|
||||
/// nodes across the queried datacenters.
|
||||
pub async fn get_consul_nodes(&self) -> Result<Vec<(NodeID, SocketAddr)>, ConsulError> {
|
||||
let url = format!(
|
||||
"{}/v1/catalog/service/{}",
|
||||
self.config.consul_http_addr, self.config.service_name
|
||||
);
|
||||
|
||||
let http = self.client.get(&url).send().await?;
|
||||
let entries: Vec<ConsulQueryEntry> = http.json().await?;
|
||||
|
||||
let mut ret = vec![];
|
||||
for ent in entries {
|
||||
let ip = ent.address.parse::<IpAddr>().ok();
|
||||
let pubkey = ent
|
||||
.meta
|
||||
.get(&format!("{}-pubkey", META_PREFIX))
|
||||
.and_then(|k| hex::decode(k).ok())
|
||||
.and_then(|k| NodeID::from_slice(&k[..]));
|
||||
if let (Some(ip), Some(pubkey)) = (ip, pubkey) {
|
||||
ret.push((pubkey, SocketAddr::new(ip, ent.service_port)));
|
||||
} else {
|
||||
warn!(
|
||||
"Could not process node spec from Consul: {:?} (invalid IP address or node ID/pubkey)",
|
||||
ent
|
||||
);
|
||||
|
||||
let dcs_to_query: Vec<Option<&str>> = match &self.config.datacenters {
|
||||
dcs if !dcs.is_empty() => dcs.iter().map(|dc| Some(dc.as_str())).collect(),
|
||||
_ => vec![None],
|
||||
};
|
||||
|
||||
for dc in dcs_to_query {
|
||||
let url = match dc {
|
||||
Some(datacenter) => format!(
|
||||
"{}/v1/catalog/service/{}?dc={}",
|
||||
self.config.consul_http_addr, self.config.service_name, datacenter
|
||||
),
|
||||
None => format!(
|
||||
"{}/v1/catalog/service/{}",
|
||||
self.config.consul_http_addr, self.config.service_name
|
||||
),
|
||||
};
|
||||
|
||||
let http = self.client.get(&url).send().await?;
|
||||
let entries: Vec<ConsulQueryEntry> = http.json().await?;
|
||||
|
||||
for ent in entries {
|
||||
let ip = ent.address.parse::<IpAddr>().ok();
|
||||
let pubkey = ent
|
||||
.meta
|
||||
.get(&format!("{}-pubkey", META_PREFIX))
|
||||
.and_then(|k| hex::decode(k).ok())
|
||||
.and_then(|k| NodeID::from_slice(&k[..]));
|
||||
if let (Some(ip), Some(pubkey)) = (ip, pubkey) {
|
||||
ret.push((pubkey, SocketAddr::new(ip, ent.service_port)));
|
||||
} else {
|
||||
warn!(
|
||||
"Could not process node spec from Consul: {:?} (invalid IP address or node ID/pubkey)",
|
||||
ent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("Got nodes from Consul: {:?}", ret);
|
||||
|
||||
debug!("Got {} nodes from Consul", ret.len());
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
// ---- PUBLISHING TO CONSUL CATALOG ----
|
||||
|
||||
pub async fn publish_consul_service(
|
||||
@@ -219,12 +242,12 @@ impl ConsulDiscovery {
|
||||
/// Regroup all Consul discovery errors
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConsulError {
|
||||
#[error(display = "IO error: {}", _0)]
|
||||
Io(#[error(source)] std::io::Error),
|
||||
#[error(display = "HTTP error: {}", _0)]
|
||||
Reqwest(#[error(source)] reqwest::Error),
|
||||
#[error(display = "Invalid Consul TLS configuration")]
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("HTTP error: {0}")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
#[error("Invalid Consul TLS configuration")]
|
||||
InvalidTLSConfig,
|
||||
#[error(display = "Token error: {}", _0)]
|
||||
Token(#[error(source)] reqwest::header::InvalidHeaderValue),
|
||||
#[error("Token error: {0}")]
|
||||
Token(#[from] reqwest::header::InvalidHeaderValue),
|
||||
}
|
||||
|
||||
@@ -517,7 +517,7 @@ impl LayoutVersion {
|
||||
g.compute_maximal_flow()?;
|
||||
if g.get_flow_value()? < (NB_PARTITIONS * self.replication_factor) as i64 {
|
||||
return Err(Error::Message(
|
||||
"The storage capacity of he cluster is to small. It is \
|
||||
"The storage capacity of the cluster is too small. It is \
|
||||
impossible to store partitions of size 1."
|
||||
.into(),
|
||||
));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_table"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_util"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -21,7 +21,7 @@ arc-swap.workspace = true
|
||||
async-trait.workspace = true
|
||||
blake2.workspace = true
|
||||
bytesize.workspace = true
|
||||
err-derive.workspace = true
|
||||
thiserror.workspace = true
|
||||
hexdump.workspace = true
|
||||
xxhash-rust.workspace = true
|
||||
hex.workspace = true
|
||||
|
||||
@@ -45,6 +45,11 @@ pub struct Config {
|
||||
)]
|
||||
pub block_size: usize,
|
||||
|
||||
/// Maximum number of parallel block writes per PUT request
|
||||
/// Higher values improve throughput but increase memory usage
|
||||
/// Default: 3, Recommended: 10-30 for NVMe, 3-10 for HDD
|
||||
#[serde(default = "default_block_max_concurrent_writes_per_request")]
|
||||
pub block_max_concurrent_writes_per_request: usize,
|
||||
/// Number of replicas. Can be any positive integer, but uneven numbers are more favorable.
|
||||
/// - 1 for single-node clusters, or to disable replication
|
||||
/// - 3 is the recommended and supported setting.
|
||||
@@ -257,6 +262,8 @@ pub struct ConsulDiscoveryConfig {
|
||||
/// Additional service metadata to add
|
||||
#[serde(default)]
|
||||
pub meta: Option<std::collections::HashMap<String, String>>,
|
||||
#[serde(default)]
|
||||
pub datacenters: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -270,6 +277,9 @@ pub struct KubernetesDiscoveryConfig {
|
||||
pub skip_crd: bool,
|
||||
}
|
||||
|
||||
pub fn default_block_max_concurrent_writes_per_request() -> usize {
|
||||
3
|
||||
}
|
||||
/// Read and parse configuration
|
||||
pub fn read_config(config_file: PathBuf) -> Result<Config, Error> {
|
||||
let config = std::fs::read_to_string(config_file)?;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
use err_derive::Error;
|
||||
use thiserror::Error;
|
||||
|
||||
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
@@ -12,71 +12,64 @@ use crate::encode::debug_serialize;
|
||||
/// Regroup all Garage errors
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(display = "IO error: {}", _0)]
|
||||
Io(#[error(source)] io::Error),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error(display = "Hyper error: {}", _0)]
|
||||
Hyper(#[error(source)] hyper::Error),
|
||||
#[error("Hyper error: {0}")]
|
||||
Hyper(#[from] hyper::Error),
|
||||
|
||||
#[error(display = "HTTP error: {}", _0)]
|
||||
Http(#[error(source)] http::Error),
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] http::Error),
|
||||
|
||||
#[error(display = "Invalid HTTP header value: {}", _0)]
|
||||
HttpHeader(#[error(source)] http::header::ToStrError),
|
||||
#[error("Invalid HTTP header value: {0}")]
|
||||
HttpHeader(#[from] http::header::ToStrError),
|
||||
|
||||
#[error(display = "Network error: {}", _0)]
|
||||
Net(#[error(source)] garage_net::error::Error),
|
||||
#[error("Network error: {0}")]
|
||||
Net(#[from] garage_net::error::Error),
|
||||
|
||||
#[error(display = "DB error: {}", _0)]
|
||||
Db(#[error(source)] garage_db::Error),
|
||||
#[error("DB error: {0}")]
|
||||
Db(#[from] garage_db::Error),
|
||||
|
||||
#[error(display = "Messagepack encode error: {}", _0)]
|
||||
RmpEncode(#[error(source)] rmp_serde::encode::Error),
|
||||
#[error(display = "Messagepack decode error: {}", _0)]
|
||||
RmpDecode(#[error(source)] rmp_serde::decode::Error),
|
||||
#[error(display = "JSON error: {}", _0)]
|
||||
Json(#[error(source)] serde_json::error::Error),
|
||||
#[error(display = "TOML decode error: {}", _0)]
|
||||
TomlDecode(#[error(source)] toml::de::Error),
|
||||
#[error("Messagepack encode error: {0}")]
|
||||
RmpEncode(#[from] rmp_serde::encode::Error),
|
||||
#[error("Messagepack decode error: {0}")]
|
||||
RmpDecode(#[from] rmp_serde::decode::Error),
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::error::Error),
|
||||
#[error("TOML decode error: {0}")]
|
||||
TomlDecode(#[from] toml::de::Error),
|
||||
|
||||
#[error(display = "Tokio join error: {}", _0)]
|
||||
TokioJoin(#[error(source)] tokio::task::JoinError),
|
||||
#[error("Tokio join error: {0}")]
|
||||
TokioJoin(#[from] tokio::task::JoinError),
|
||||
|
||||
#[error(display = "Tokio semaphore acquire error: {}", _0)]
|
||||
TokioSemAcquire(#[error(source)] tokio::sync::AcquireError),
|
||||
#[error("Tokio semaphore acquire error: {0}")]
|
||||
TokioSemAcquire(#[from] tokio::sync::AcquireError),
|
||||
|
||||
#[error(display = "Tokio broadcast receive error: {}", _0)]
|
||||
TokioBcastRecv(#[error(source)] tokio::sync::broadcast::error::RecvError),
|
||||
#[error("Tokio broadcast receive error: {0}")]
|
||||
TokioBcastRecv(#[from] tokio::sync::broadcast::error::RecvError),
|
||||
|
||||
#[error(display = "Remote error: {}", _0)]
|
||||
#[error("Remote error: {0}")]
|
||||
RemoteError(String),
|
||||
|
||||
#[error(display = "Timeout")]
|
||||
#[error("Timeout")]
|
||||
Timeout,
|
||||
|
||||
#[error(display = "Layout not ready")]
|
||||
#[error("Layout not ready")]
|
||||
LayoutNotReady,
|
||||
|
||||
#[error(
|
||||
display = "Could not reach quorum of {} (sets={:?}). {} of {} request succeeded, others returned errors: {:?}",
|
||||
_0,
|
||||
_1,
|
||||
_2,
|
||||
_3,
|
||||
_4
|
||||
)]
|
||||
#[error("Could not reach quorum of {0} (sets={1:?}). {2} of {3} request succeeded, others returned errors: {4:?}")]
|
||||
Quorum(usize, Option<usize>, usize, usize, Vec<String>),
|
||||
|
||||
#[error(display = "Unexpected RPC message: {}", _0)]
|
||||
#[error("Unexpected RPC message: {0}")]
|
||||
UnexpectedRpcMessage(String),
|
||||
|
||||
#[error(display = "Corrupt data: does not match hash {:?}", _0)]
|
||||
#[error("Corrupt data: does not match hash {0:?}")]
|
||||
CorruptData(Hash),
|
||||
|
||||
#[error(display = "Missing block {:?}: no node returned a valid block", _0)]
|
||||
#[error("Missing block {0:?}: no node returned a valid block")]
|
||||
MissingBlock(Hash),
|
||||
|
||||
#[error(display = "{}", _0)]
|
||||
#[error("{0}")]
|
||||
Message(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_web"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -20,7 +20,7 @@ garage_model.workspace = true
|
||||
garage_util.workspace = true
|
||||
garage_table.workspace = true
|
||||
|
||||
err-derive.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use err_derive::Error;
|
||||
use hyper::header::HeaderValue;
|
||||
use hyper::{HeaderMap, StatusCode};
|
||||
use thiserror::Error;
|
||||
|
||||
use garage_api_common::generic_server::ApiError;
|
||||
|
||||
@@ -8,15 +8,15 @@ use garage_api_common::generic_server::ApiError;
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// An error received from the API crate
|
||||
#[error(display = "API error: {}", _0)]
|
||||
#[error("API error: {0}")]
|
||||
ApiError(garage_api_s3::error::Error),
|
||||
|
||||
/// The file does not exist
|
||||
#[error(display = "Not found")]
|
||||
#[error("Not found")]
|
||||
NotFound,
|
||||
|
||||
/// The client sent a request without host, or with unsupported method
|
||||
#[error(display = "Bad request: {}", _0)]
|
||||
#[error("Bad request: {0}")]
|
||||
BadRequest(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -579,7 +579,7 @@ fn path_to_keys(
|
||||
};
|
||||
if let Some(condition) = &routing_rule.condition {
|
||||
let suffix = if let Some(prefix) = &condition.prefix {
|
||||
let Some(suffix) = key.strip_prefix(prefix) else {
|
||||
let Some(suffix) = base_key.strip_prefix(prefix) else {
|
||||
continue;
|
||||
};
|
||||
Some(suffix)
|
||||
|
||||