Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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:
|
steps:
|
||||||
- name: check formatting
|
- name: check formatting
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-shell --attr devShell --run "cargo fmt -- --check"
|
- nix-build -j4 --attr flakePackages.fmt
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-build -j4 --attr flakePackages.dev
|
- nix-build -j4 --attr flakePackages.dev
|
||||||
|
|
||||||
- name: unit + func tests (lmdb)
|
- name: unit + func tests (lmdb)
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-build -j4 --attr flakePackages.tests-lmdb
|
- nix-build -j4 --attr flakePackages.tests-lmdb
|
||||||
|
|
||||||
- name: unit + func tests (sqlite)
|
- name: unit + func tests (sqlite)
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-build -j4 --attr flakePackages.tests-sqlite
|
- nix-build -j4 --attr flakePackages.tests-sqlite
|
||||||
|
|
||||||
- name: unit + func tests (fjall)
|
- name: unit + func tests (fjall)
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-build -j4 --attr flakePackages.tests-fjall
|
- nix-build -j4 --attr flakePackages.tests-fjall
|
||||||
|
|
||||||
- name: integration tests
|
- name: integration tests
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-build -j4 --attr flakePackages.dev
|
- nix-build -j4 --attr flakePackages.dev
|
||||||
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ depends_on:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: refresh-index
|
- name: refresh-index
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
environment:
|
environment:
|
||||||
AWS_ACCESS_KEY_ID:
|
AWS_ACCESS_KEY_ID:
|
||||||
from_secret: garagehq_aws_access_key_id
|
from_secret: garagehq_aws_access_key_id
|
||||||
@@ -22,7 +22,7 @@ steps:
|
|||||||
- nix-shell --attr ci --run "refresh_index"
|
- nix-shell --attr ci --run "refresh_index"
|
||||||
|
|
||||||
- name: multiarch-docker
|
- name: multiarch-docker
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
environment:
|
environment:
|
||||||
DOCKER_AUTH:
|
DOCKER_AUTH:
|
||||||
from_secret: docker_auth
|
from_secret: docker_auth
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ matrix:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build
|
- name: build
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-build --attr releasePackages.${ARCH} --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
|
- nix-build --attr releasePackages.${ARCH} --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
|
||||||
|
|
||||||
- name: check is static binary
|
- name: check is static binary
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-shell --attr ci --run "./script/not-dynamic.sh result/bin/garage"
|
- nix-shell --attr ci --run "./script/not-dynamic.sh result/bin/garage"
|
||||||
|
|
||||||
- name: integration tests
|
- name: integration tests
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||||
when:
|
when:
|
||||||
@@ -39,7 +39,7 @@ steps:
|
|||||||
ARCH: i386
|
ARCH: i386
|
||||||
|
|
||||||
- name: upgrade tests from v1.0.0
|
- name: upgrade tests from v1.0.0
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-shell --attr ci --run "./script/test-upgrade.sh v1.0.0 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
|
- nix-shell --attr ci --run "./script/test-upgrade.sh v1.0.0 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
|
||||||
when:
|
when:
|
||||||
@@ -47,7 +47,7 @@ steps:
|
|||||||
ARCH: amd64
|
ARCH: amd64
|
||||||
|
|
||||||
- name: upgrade tests from v0.8.4
|
- name: upgrade tests from v0.8.4
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
commands:
|
commands:
|
||||||
- nix-shell --attr ci --run "./script/test-upgrade.sh v0.8.4 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
|
- nix-shell --attr ci --run "./script/test-upgrade.sh v0.8.4 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
|
||||||
when:
|
when:
|
||||||
@@ -55,7 +55,7 @@ steps:
|
|||||||
ARCH: amd64
|
ARCH: amd64
|
||||||
|
|
||||||
- name: push static binary
|
- name: push static binary
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
environment:
|
environment:
|
||||||
TARGET: "${TARGET}"
|
TARGET: "${TARGET}"
|
||||||
AWS_ACCESS_KEY_ID:
|
AWS_ACCESS_KEY_ID:
|
||||||
@@ -66,7 +66,7 @@ steps:
|
|||||||
- nix-shell --attr ci --run "to_s3"
|
- nix-shell --attr ci --run "to_s3"
|
||||||
|
|
||||||
- name: docker build and publish
|
- name: docker build and publish
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-24.05
|
||||||
environment:
|
environment:
|
||||||
DOCKER_PLATFORM: "linux/${ARCH}"
|
DOCKER_PLATFORM: "linux/${ARCH}"
|
||||||
CONTAINER_NAME: "dxflrs/${ARCH}_garage"
|
CONTAINER_NAME: "dxflrs/${ARCH}_garage"
|
||||||
|
|||||||
@@ -24,18 +24,18 @@ default-members = ["src/garage"]
|
|||||||
|
|
||||||
# Internal Garage crates
|
# Internal Garage crates
|
||||||
format_table = { version = "0.1.1", path = "src/format-table" }
|
format_table = { version = "0.1.1", path = "src/format-table" }
|
||||||
garage_api_common = { version = "2.1.0", path = "src/api/common" }
|
garage_api_common = { version = "2.2.0", path = "src/api/common" }
|
||||||
garage_api_admin = { version = "2.1.0", path = "src/api/admin" }
|
garage_api_admin = { version = "2.2.0", path = "src/api/admin" }
|
||||||
garage_api_s3 = { version = "2.1.0", path = "src/api/s3" }
|
garage_api_s3 = { version = "2.2.0", path = "src/api/s3" }
|
||||||
garage_api_k2v = { version = "2.1.0", path = "src/api/k2v" }
|
garage_api_k2v = { version = "2.2.0", path = "src/api/k2v" }
|
||||||
garage_block = { version = "2.1.0", path = "src/block" }
|
garage_block = { version = "2.2.0", path = "src/block" }
|
||||||
garage_db = { version = "2.1.0", path = "src/db", default-features = false }
|
garage_db = { version = "2.2.0", path = "src/db", default-features = false }
|
||||||
garage_model = { version = "2.1.0", path = "src/model", default-features = false }
|
garage_model = { version = "2.2.0", path = "src/model", default-features = false }
|
||||||
garage_net = { version = "2.1.0", path = "src/net" }
|
garage_net = { version = "2.2.0", path = "src/net" }
|
||||||
garage_rpc = { version = "2.1.0", path = "src/rpc" }
|
garage_rpc = { version = "2.2.0", path = "src/rpc" }
|
||||||
garage_table = { version = "2.1.0", path = "src/table" }
|
garage_table = { version = "2.2.0", path = "src/table" }
|
||||||
garage_util = { version = "2.1.0", path = "src/util" }
|
garage_util = { version = "2.2.0", path = "src/util" }
|
||||||
garage_web = { version = "2.1.0", path = "src/web" }
|
garage_web = { version = "2.2.0", path = "src/web" }
|
||||||
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
|
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
|
||||||
|
|
||||||
# External crates from crates.io
|
# External crates from crates.io
|
||||||
@@ -49,9 +49,8 @@ bytes = "1.0"
|
|||||||
bytesize = "1.1"
|
bytesize = "1.1"
|
||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
crc-fast = "1.1"
|
crc-fast = "1.6"
|
||||||
crypto-common = "0.1"
|
crypto-common = "0.1"
|
||||||
err-derive = "0.3"
|
|
||||||
gethostname = "0.4"
|
gethostname = "0.4"
|
||||||
git-version = "0.3.4"
|
git-version = "0.3.4"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
@@ -138,7 +137,7 @@ prometheus = "0.13"
|
|||||||
aws-sigv4 = { version = "1.1", default-features = false }
|
aws-sigv4 = { version = "1.1", default-features = false }
|
||||||
hyper-rustls = { version = "0.26", default-features = false, features = ["http1", "http2", "ring", "rustls-native-certs"] }
|
hyper-rustls = { version = "0.26", default-features = false, features = ["http1", "http2", "ring", "rustls-native-certs"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
thiserror = "1.0"
|
thiserror = "2.0"
|
||||||
|
|
||||||
# ---- used only as build / dev dependencies ----
|
# ---- used only as build / dev dependencies ----
|
||||||
assert-json-diff = "2.0"
|
assert-json-diff = "2.0"
|
||||||
@@ -155,5 +154,5 @@ lto = "off"
|
|||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
opt-level = "s"
|
opt-level = 3
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"name": "AGPL-3.0",
|
"name": "AGPL-3.0",
|
||||||
"identifier": "AGPL-3.0"
|
"identifier": "AGPL-3.0"
|
||||||
},
|
},
|
||||||
"version": "v2.1.0"
|
"version": "v2.2.0"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/AddBucketAliasRequest"
|
"$ref": "#/components/schemas/BucketAliasEnum"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1409,7 +1409,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/RemoveBucketAliasRequest"
|
"$ref": "#/components/schemas/BucketAliasEnum"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1722,24 +1722,6 @@
|
|||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"AddBucketAliasRequest": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/BucketAliasEnum"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"bucketId"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"bucketId": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"AddBucketAliasResponse": {
|
"AddBucketAliasResponse": {
|
||||||
"$ref": "#/components/schemas/GetBucketInfoResponse"
|
"$ref": "#/components/schemas/GetBucketInfoResponse"
|
||||||
},
|
},
|
||||||
@@ -1957,9 +1939,13 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"bucketId",
|
||||||
"globalAlias"
|
"globalAlias"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"bucketId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"globalAlias": {
|
"globalAlias": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -1968,6 +1954,7 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"bucketId",
|
||||||
"localAlias",
|
"localAlias",
|
||||||
"accessKeyId"
|
"accessKeyId"
|
||||||
],
|
],
|
||||||
@@ -1975,6 +1962,9 @@
|
|||||||
"accessKeyId": {
|
"accessKeyId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"bucketId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"localAlias": {
|
"localAlias": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -3912,6 +3902,46 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NodeAssignedRole"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the node for which this change applies"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"NodeUpdateTrackers": {
|
"NodeUpdateTrackers": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3973,24 +4003,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"RemoveBucketAliasRequest": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/BucketAliasEnum"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"bucketId"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"bucketId": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"RemoveBucketAliasResponse": {
|
"RemoveBucketAliasResponse": {
|
||||||
"$ref": "#/components/schemas/GetBucketInfoResponse"
|
"$ref": "#/components/schemas/GetBucketInfoResponse"
|
||||||
},
|
},
|
||||||
@@ -4180,7 +4192,7 @@
|
|||||||
"roles": {
|
"roles": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/NodeRoleChange"
|
"$ref": "#/components/schemas/NodeRoleChangeRequest"
|
||||||
},
|
},
|
||||||
"description": "New node roles to assign or remove in the cluster layout"
|
"description": "New node roles to assign or remove in the cluster layout"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ In this section, we cover the following web applications:
|
|||||||
| [Mastodon](#mastodon) | ✅ | Natively supported |
|
| [Mastodon](#mastodon) | ✅ | Natively supported |
|
||||||
| [Matrix](#matrix) | ✅ | Tested with `synapse-s3-storage-provider` |
|
| [Matrix](#matrix) | ✅ | Tested with `synapse-s3-storage-provider` |
|
||||||
| [ejabberd](#ejabberd) | ✅ | `mod_s3_upload` |
|
| [ejabberd](#ejabberd) | ✅ | `mod_s3_upload` |
|
||||||
| [Pixelfed](#pixelfed) | ✅ | Natively supported |
|
| [Ente](#ente) | ✅ | Natively supported |
|
||||||
| [Pleroma](#pleroma) | ❓ | Not yet tested |
|
| [Pixelfed](#pixelfed) | ❓ | Natively supported |
|
||||||
|
| [Pleroma](#pleroma) | ✅ | Natively supported |
|
||||||
| [Lemmy](#lemmy) | ✅ | Supported with pict-rs |
|
| [Lemmy](#lemmy) | ✅ | Supported with pict-rs |
|
||||||
| [Funkwhale](#funkwhale) | ❓ | Not yet tested |
|
| [Funkwhale](#funkwhale) | ❓ | Not yet tested |
|
||||||
| [Misskey](#misskey) | ❓ | 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
|
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).
|
[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
|
||||||
|
|
||||||
[Pixelfed Technical Documentation > Configuration](https://docs.pixelfed.org/technical-documentation/env.html#filesystem)
|
[Pixelfed Technical Documentation > Configuration](https://docs.pixelfed.org/technical-documentation/env.html#filesystem)
|
||||||
|
|
||||||
## Pleroma
|
## 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
|
## Lemmy
|
||||||
|
|
||||||
|
|||||||
@@ -207,3 +207,13 @@ $ plakar at @garageS3 ls
|
|||||||
```
|
```
|
||||||
|
|
||||||
More information in Plakar documentation: https://www.plakar.io/docs/main/quickstart/
|
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
|
This will tremendously accelerate operations such as `rclone sync` or `rclone ncdu` by reducing the number
|
||||||
of ListObjects calls that are made.
|
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`
|
## `s3cmd`
|
||||||
|
|
||||||
@@ -314,4 +323,3 @@ ls
|
|||||||
```
|
```
|
||||||
|
|
||||||
And through the web interface at http://[::1]:8080/web/client
|
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
|
apk add garage
|
||||||
```
|
```
|
||||||
|
|
||||||
The default configuration file is installed to `/etc/garage.toml`. You can run
|
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
|
Garage using: `rc-service garage start`.
|
||||||
will be automatically replaced with a random string on the first 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
|
Please note that this package is built without Consul discovery, Kubernetes
|
||||||
discovery, OpenTelemetry exporter, and K2V features (K2V will be enabled once
|
discovery, OpenTelemetry exporter, and K2V features (K2V will be enabled once
|
||||||
@@ -26,7 +27,11 @@ it's stable).
|
|||||||
|
|
||||||
## Arch Linux
|
## 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
|
## FreeBSD
|
||||||
|
|
||||||
@@ -39,3 +44,9 @@ pkg install garage
|
|||||||
```bash
|
```bash
|
||||||
nix-shell -p garage
|
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
|
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
|
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
|
which contains all of the released versions as well as the code
|
||||||
for the developpement of the next version.
|
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 |
|
| Feature flag | Enabled | Description |
|
||||||
| ------------ | ------- | ----------- |
|
| ------------ | ------- | ----------- |
|
||||||
| `bundled-libs` | *by default* | Use bundled version of sqlite3, zstd, lmdb and libsodium |
|
| `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) |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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
|
```bash
|
||||||
git clone https://git.deuxfleurs.fr/Deuxfleurs/garage
|
git clone https://git.deuxfleurs.fr/Deuxfleurs/garage
|
||||||
cd garage/scripts/helm
|
cd garage/script/helm
|
||||||
```
|
```
|
||||||
|
|
||||||
Deploy with default options:
|
Deploy with default options:
|
||||||
|
|||||||
@@ -96,14 +96,14 @@ to store 2 TB of data in total.
|
|||||||
## Get a Docker image
|
## 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).
|
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.
|
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.1.0` but it's up to you
|
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).
|
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:
|
For example:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo docker pull dxflrs/garage:v2.1.0
|
sudo docker pull dxflrs/garage:v2.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploying and configuring Garage
|
## Deploying and configuring Garage
|
||||||
@@ -171,7 +171,7 @@ docker run \
|
|||||||
-v /etc/garage.toml:/etc/garage.toml \
|
-v /etc/garage.toml:/etc/garage.toml \
|
||||||
-v /var/lib/garage/meta:/var/lib/garage/meta \
|
-v /var/lib/garage/meta:/var/lib/garage/meta \
|
||||||
-v /var/lib/garage/data:/var/lib/garage/data \
|
-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.
|
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"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
garage:
|
garage:
|
||||||
image: dxflrs/garage:v2.1.0
|
image: dxflrs/garage:v2.2.0
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
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.
|
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)
|
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:
|
If you are only testing Garage, you can generate a self-signed certificate to follow the documentation:
|
||||||
|
|
||||||
|
|||||||
@@ -162,3 +162,6 @@ your recovery options are as follows:
|
|||||||
- **Option 3: restoring a filesystem-level snapshot.** If you are using ZFS or
|
- **Option 3: restoring a filesystem-level snapshot.** If you are using ZFS or
|
||||||
BTRFS to snapshot your metadata partition, refer to their specific
|
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.toml:/etc/garage.toml \
|
||||||
-v /path/to/garage/meta:/var/lib/garage/meta \
|
-v /path/to/garage/meta:/var/lib/garage/meta \
|
||||||
-v /path/to/garage/data:/var/lib/garage/data \
|
-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`
|
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_size = "1M"
|
||||||
block_ram_buffer_max = "256MiB"
|
block_ram_buffer_max = "256MiB"
|
||||||
block_max_concurrent_reads = 16
|
block_max_concurrent_reads = 16
|
||||||
|
block_max_concurrent_writes_per_request =10
|
||||||
lmdb_map_size = "1T"
|
lmdb_map_size = "1T"
|
||||||
|
|
||||||
compression_level = 1
|
compression_level = 1
|
||||||
@@ -51,17 +51,20 @@ allow_punycode = false
|
|||||||
|
|
||||||
[consul_discovery]
|
[consul_discovery]
|
||||||
api = "catalog"
|
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"
|
service_name = "garage-daemon"
|
||||||
|
|
||||||
ca_cert = "/etc/consul/consul-ca.crt"
|
ca_cert = "/etc/consul/consul-ca.crt"
|
||||||
client_cert = "/etc/consul/consul-client.crt"
|
client_cert = "/etc/consul/consul-client.crt"
|
||||||
client_key = "/etc/consul/consul-key.crt"
|
client_key = "/etc/consul/consul-key.crt"
|
||||||
|
|
||||||
# for `agent` API mode, unset client_cert and client_key, and optionally enable `token`
|
# for `agent` API mode, unset client_cert and client_key, and optionally enable `token`
|
||||||
# token = "abcdef-01234-56789"
|
# token = "abcdef-01234-56789"
|
||||||
tls_skip_verify = false
|
|
||||||
tags = [ "dns-enabled" ]
|
tags = [ "dns-enabled" ]
|
||||||
meta = { dns-acl = "allow trusted" }
|
meta = { dns-acl = "allow trusted" }
|
||||||
|
datacenters = ["dc1", "dc2", "dc3"]
|
||||||
|
|
||||||
[kubernetes_discovery]
|
[kubernetes_discovery]
|
||||||
namespace = "garage"
|
namespace = "garage"
|
||||||
@@ -98,7 +101,8 @@ The following gives details about each available configuration option.
|
|||||||
Top-level configuration options, in alphabetical order:
|
Top-level configuration options, in alphabetical order:
|
||||||
[`allow_punycode`](#allow_punycode),
|
[`allow_punycode`](#allow_punycode),
|
||||||
[`allow_world_readable_secrets`](#allow_world_readable_secrets),
|
[`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_ram_buffer_max`](#block_ram_buffer_max),
|
||||||
[`block_size`](#block_size),
|
[`block_size`](#block_size),
|
||||||
[`bootstrap_peers`](#bootstrap_peers),
|
[`bootstrap_peers`](#bootstrap_peers),
|
||||||
@@ -127,12 +131,14 @@ The `[consul_discovery]` section:
|
|||||||
[`client_cert`](#consul_client_cert_and_key),
|
[`client_cert`](#consul_client_cert_and_key),
|
||||||
[`client_key`](#consul_client_cert_and_key),
|
[`client_key`](#consul_client_cert_and_key),
|
||||||
[`consul_http_addr`](#consul_http_addr),
|
[`consul_http_addr`](#consul_http_addr),
|
||||||
|
[`datacenters`](#consul_datacenters)
|
||||||
[`meta`](#consul_tags_and_meta),
|
[`meta`](#consul_tags_and_meta),
|
||||||
[`service_name`](#consul_service_name),
|
[`service_name`](#consul_service_name),
|
||||||
[`tags`](#consul_tags_and_meta),
|
[`tags`](#consul_tags_and_meta),
|
||||||
[`tls_skip_verify`](#consul_tls_skip_verify),
|
[`tls_skip_verify`](#consul_tls_skip_verify),
|
||||||
[`token`](#consul_token).
|
[`token`](#consul_token).
|
||||||
|
|
||||||
|
|
||||||
The `[kubernetes_discovery]` section:
|
The `[kubernetes_discovery]` section:
|
||||||
[`namespace`](#kube_namespace),
|
[`namespace`](#kube_namespace),
|
||||||
[`service_name`](#kube_service_name),
|
[`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/` |
|
| [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` |
|
| [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](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.
|
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:
|
Performance characteristics of the different DB engines are as follows:
|
||||||
|
|
||||||
- LMDB: the recommended database engine for high-performance distributed clusters.
|
- **LMDB:** the recommended database engine for high-performance distributed clusters
|
||||||
LMDB works very well, but is known to have the following limitations:
|
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
|
- 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
|
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
|
node to very small database sizes due to how LMDB works; it is therefore
|
||||||
not recommended.
|
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
|
- 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.
|
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
|
- **Sqlite:** Garage supports Sqlite as an alternative storage backend for
|
||||||
metadata, which does not have the issues listed above for LMDB.
|
metadata, which does not have the issues listed above for LMDB. Sqlite is
|
||||||
On versions 0.8.x and earlier, Sqlite should be avoided due to abysmal
|
slower than LMDB, so it is not the best choice for high-performance storage
|
||||||
performance, which was fixed with the addition of `metadata_fsync`.
|
clusters.
|
||||||
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.
|
|
||||||
|
|
||||||
- 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.
|
higher write throughput than other storage engines that are based on B-trees.
|
||||||
Using Fjall could potentially improve Garage's performance significantly in
|
Using Fjall could potentially improve Garage's performance significantly in
|
||||||
write-heavy workloads. **Support for Fjall is experimental at this point**,
|
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
|
we have added it to Garage for evaluation purposes only. **Use it only with
|
||||||
production-critical workloads.**
|
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
|
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:
|
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}
|
#### `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
|
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
|
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.
|
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.
|
page](@/documentation/operations/recovering.md#corrupted_meta) for details.
|
||||||
Garage keeps only the two most recent snapshots of the metadata DB and deletes
|
Garage keeps only the two most recent snapshots of the metadata DB and deletes
|
||||||
older ones automatically.
|
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
|
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
|
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
|
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
|
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.
|
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}
|
#### `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.
|
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}
|
#### `lmdb_map_size` {#lmdb_map_size}
|
||||||
|
|
||||||
This parameters can be used to set the map size used by LMDB,
|
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}
|
#### `tags` and `meta` {#consul_tags_and_meta}
|
||||||
|
|
||||||
Additional list of tags and map of service meta to add during service registration.
|
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) |
|
| 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 | ✅ | ✅ | ❌ | ✅ |
|
| [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 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 | ❌| ✅| ✅ | ✅ |
|
| [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": {
|
"original": {
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
|
"rev": "6fe74265bbb6d016d663b1091f015e2976c4a527",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1717312683,
|
"lastModified": 1761640442,
|
||||||
"narHash": "sha256-FrlieJH50AuvagamEvWMIE6D2OAnERuDboFDYAED/dE=",
|
"narHash": "sha256-AtrEP6Jmdvrqiv4x2xa5mrtaIp3OEe8uBYCDZDS+hu8=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "flake-compat",
|
"repo": "flake-compat",
|
||||||
"rev": "38fd3954cf65ce6faf3d0d45cd26059e059f07ea",
|
"rev": "4a56054d8ffc173222d09dad23adf4ba946c8884",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -50,17 +51,17 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1747825515,
|
"lastModified": 1763977559,
|
||||||
"narHash": "sha256-BWpMQymVI73QoKZdcVCxUCCK3GNvr/xa2Dc4DM1o2BE=",
|
"narHash": "sha256-g4MKqsIRy5yJwEsI+fYODqLUnAqIY4kZai0nldAP6EM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "cd2812de55cf87df88a9e09bf3be1ce63d50c1a6",
|
"rev": "cfe2c7d5b5d3032862254e68c37a6576b633d632",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "cd2812de55cf87df88a9e09bf3be1ce63d50c1a6",
|
"rev": "cfe2c7d5b5d3032862254e68c37a6576b633d632",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -80,17 +81,17 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1738549608,
|
"lastModified": 1763952169,
|
||||||
"narHash": "sha256-GdyT9QEUSx5k/n8kILuNy83vxxdyUfJ8jL5mMpQZWfw=",
|
"narHash": "sha256-+PeDBD8P+NKauH+w7eO/QWCIp8Cx4mCfWnh9sJmy9CM=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "35c6f8c4352f995ecd53896200769f80a3e8f22d",
|
"rev": "ab726555a9a72e6dc80649809147823a813fa95b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "35c6f8c4352f995ecd53896200769f80a3e8f22d",
|
"rev": "ab726555a9a72e6dc80649809147823a813fa95b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,16 +2,17 @@
|
|||||||
description =
|
description =
|
||||||
"Garage, an S3-compatible distributed object store for self-hosted deployments";
|
"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 =
|
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 =
|
inputs.rust-overlay.url =
|
||||||
"github:oxalica/rust-overlay/35c6f8c4352f995ecd53896200769f80a3e8f22d";
|
"github:oxalica/rust-overlay/ab726555a9a72e6dc80649809147823a813fa95b";
|
||||||
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
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-compat.url = "github:nix-community/flake-compat";
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
@@ -30,6 +31,10 @@
|
|||||||
inherit system nixpkgs crane rust-overlay extraTestEnv;
|
inherit system nixpkgs crane rust-overlay extraTestEnv;
|
||||||
release = false;
|
release = false;
|
||||||
}).garage-test;
|
}).garage-test;
|
||||||
|
lints = (compile {
|
||||||
|
inherit system nixpkgs crane rust-overlay;
|
||||||
|
release = false;
|
||||||
|
});
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages = {
|
packages = {
|
||||||
@@ -56,6 +61,10 @@
|
|||||||
tests-fjall = testWith {
|
tests-fjall = testWith {
|
||||||
GARAGE_TEST_INTEGRATION_DB_ENGINE = "fjall";
|
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 ----
|
# ---- developpment shell, for making native builds only ----
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ let
|
|||||||
</ul></p>
|
</ul></p>
|
||||||
<p> Sources:
|
<p> Sources:
|
||||||
<ul>
|
<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}.zip">.zip</a></li>
|
||||||
<li><a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/archive/${x.version}.tar.gz">.tar.gz</a></li>
|
<li><a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/archive/${x.version}.tar.gz">.tar.gz</a></li>
|
||||||
</ul></p>
|
</ul></p>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ let
|
|||||||
|
|
||||||
inherit (pkgs) lib stdenv;
|
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 ];
|
targets = lib.optionals (target != null) [ rustTarget ];
|
||||||
extensions = [
|
extensions = [
|
||||||
"rust-src"
|
"rust-src"
|
||||||
@@ -190,4 +190,15 @@ in rec {
|
|||||||
pkgs.cacert
|
pkgs.cacert
|
||||||
];
|
];
|
||||||
} // extraTestEnv);
|
} // 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
|
name: garage
|
||||||
description: S3-compatible object store for small self-hosted geo-distributed deployments
|
description: S3-compatible object store for small self-hosted geo-distributed deployments
|
||||||
type: application
|
type: application
|
||||||
version: 0.9.1
|
version: 0.9.2
|
||||||
appVersion: "v2.1.0"
|
appVersion: "v2.2.0"
|
||||||
home: https://garagehq.deuxfleurs.fr/
|
home: https://garagehq.deuxfleurs.fr/
|
||||||
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
|
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# garage
|
# garage
|
||||||
|
|
||||||
  
|
  
|
||||||
|
|
||||||
S3-compatible object store for small self-hosted geo-distributed deployments
|
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
|
Create the name of the rpc secret
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "garage.rpcSecretName" -}}
|
{{- define "garage.rpcSecretName" -}}
|
||||||
{{- printf "%s-rpc-secret" (include "garage.fullname" .) -}}
|
{{- .Values.garage.existingRpcSecret | default (printf "%s-rpc-secret" (include "garage.fullname" .)) -}}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
@@ -47,8 +47,8 @@ helm.sh/chart: {{ include "garage.chart" . }}
|
|||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
{{ with .Values.commonLabels }}
|
{{- with .Values.commonLabels }}
|
||||||
{{- toYaml . }}
|
{{- toYaml . | nindent 0 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,15 @@ data:
|
|||||||
# rpc_secret will be populated by the init container from a k8s secret object
|
# rpc_secret will be populated by the init container from a k8s secret object
|
||||||
rpc_secret = "__RPC_SECRET_REPLACE__"
|
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]
|
[kubernetes_discovery]
|
||||||
namespace = "{{ .Release.Namespace }}"
|
namespace = "{{ .Release.Namespace }}"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{{- if not .Values.garage.existingRpcSecret }}
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
@@ -12,3 +13,4 @@ data:
|
|||||||
{{- $prevRpcSecret := $prevSecretData.rpcSecret | default "" | b64dec }}
|
{{- $prevRpcSecret := $prevSecretData.rpcSecret | default "" | b64dec }}
|
||||||
{{/* Priority is: 1. from values, 2. previous value, 3. generate random */}}
|
{{/* Priority is: 1. from values, 2. previous value, 3. generate random */}}
|
||||||
rpcSecret: {{ .Values.garage.rpcSecret | default $prevRpcSecret | default (include "jupyterhub.randHex" 64) | b64enc | quote }}
|
rpcSecret: {{ .Values.garage.rpcSecret | default $prevRpcSecret | default (include "jupyterhub.randHex" 64) | b64enc | quote }}
|
||||||
|
{{- end }}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ metadata:
|
|||||||
name: {{ include "garage.fullname" . }}
|
name: {{ include "garage.fullname" . }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "garage.labels" . | nindent 4 }}
|
{{- include "garage.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.service.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
type: {{ .Values.service.type }}
|
type: {{ .Values.service.type }}
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ garage:
|
|||||||
rpcBindAddr: "[::]:3901"
|
rpcBindAddr: "[::]:3901"
|
||||||
# -- If not given, a random secret will be generated and stored in a Secret object
|
# -- If not given, a random secret will be generated and stored in a Secret object
|
||||||
rpcSecret: ""
|
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
|
# -- This is not required if you use the integrated kubernetes discovery
|
||||||
bootstrapPeers: []
|
bootstrapPeers: []
|
||||||
# -- Set to true if you want to use k8s discovery but install the CRDs manually outside
|
# -- 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"
|
rootDomain: ".web.garage.tld"
|
||||||
index: "index.html"
|
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 not empty string, allow using an existing ConfigMap for the garage.toml,
|
||||||
# if set, ignores garage.toml
|
# if set, ignores garage.toml
|
||||||
existingConfigMap: ""
|
existingConfigMap: ""
|
||||||
@@ -117,6 +127,7 @@ podSecurityContext:
|
|||||||
runAsUser: 1000
|
runAsUser: 1000
|
||||||
runAsGroup: 1000
|
runAsGroup: 1000
|
||||||
fsGroup: 1000
|
fsGroup: 1000
|
||||||
|
fsGroupChangePolicy: "OnRootMismatch"
|
||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
|
|
||||||
securityContext:
|
securityContext:
|
||||||
@@ -133,6 +144,8 @@ service:
|
|||||||
# - NodePort (+ Ingress)
|
# - NodePort (+ Ingress)
|
||||||
# - LoadBalancer
|
# - LoadBalancer
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
|
# -- Annotations to add to the service
|
||||||
|
annotations: {}
|
||||||
s3:
|
s3:
|
||||||
api:
|
api:
|
||||||
port: 3900
|
port: 3900
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ in
|
|||||||
s3cmd
|
s3cmd
|
||||||
minio-client
|
minio-client
|
||||||
rclone
|
rclone
|
||||||
(python312.withPackages (ps: [ ps.boto3 ]))
|
(python313.withPackages (ps: [ ps.boto3 ]))
|
||||||
|
|
||||||
socat
|
socat
|
||||||
psmisc
|
psmisc
|
||||||
@@ -36,6 +36,8 @@ in
|
|||||||
jq
|
jq
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
export AWS_REQUEST_CHECKSUM_CALCULATION='when_required'
|
||||||
|
|
||||||
function to_s3 {
|
function to_s3 {
|
||||||
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED \
|
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED \
|
||||||
aws \
|
aws \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_api_admin"
|
name = "garage_api_admin"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -26,7 +26,7 @@ argon2.workspace = true
|
|||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
bytesize.workspace = true
|
bytesize.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
err-derive.workspace = true
|
thiserror.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
paste.workspace = true
|
paste.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use err_derive::Error;
|
|
||||||
use hyper::header::HeaderValue;
|
use hyper::header::HeaderValue;
|
||||||
use hyper::{HeaderMap, StatusCode};
|
use hyper::{HeaderMap, StatusCode};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
pub use garage_model::helper::error::Error as HelperError;
|
pub use garage_model::helper::error::Error as HelperError;
|
||||||
|
|
||||||
@@ -16,36 +16,33 @@ use garage_api_common::helpers::*;
|
|||||||
/// Errors of this crate
|
/// Errors of this crate
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error(display = "{}", _0)]
|
#[error("{0}")]
|
||||||
/// Error from common error
|
/// Error from common error
|
||||||
Common(#[error(source)] CommonError),
|
Common(#[from] CommonError),
|
||||||
|
|
||||||
// Category: cannot process
|
// Category: cannot process
|
||||||
/// The admin API token does not exist
|
/// The admin API token does not exist
|
||||||
#[error(display = "Admin token not found: {}", _0)]
|
#[error("Admin token not found: {0}")]
|
||||||
NoSuchAdminToken(String),
|
NoSuchAdminToken(String),
|
||||||
|
|
||||||
/// The API access key does not exist
|
/// The API access key does not exist
|
||||||
#[error(display = "Access key not found: {}", _0)]
|
#[error("Access key not found: {0}")]
|
||||||
NoSuchAccessKey(String),
|
NoSuchAccessKey(String),
|
||||||
|
|
||||||
/// The requested block does not exist
|
/// The requested block does not exist
|
||||||
#[error(display = "Block not found: {}", _0)]
|
#[error("Block not found: {0}")]
|
||||||
NoSuchBlock(String),
|
NoSuchBlock(String),
|
||||||
|
|
||||||
/// The requested worker does not exist
|
/// The requested worker does not exist
|
||||||
#[error(display = "Worker not found: {}", _0)]
|
#[error("Worker not found: {0}")]
|
||||||
NoSuchWorker(u64),
|
NoSuchWorker(u64),
|
||||||
|
|
||||||
/// The object requested don't exists
|
/// The object requested don't exists
|
||||||
#[error(display = "Key not found")]
|
#[error("Key not found")]
|
||||||
NoSuchKey,
|
NoSuchKey,
|
||||||
|
|
||||||
/// In Import key, the key already exists
|
/// In Import key, the key already exists
|
||||||
#[error(
|
#[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.")]
|
||||||
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
|
|
||||||
)]
|
|
||||||
KeyAlreadyExists(String),
|
KeyAlreadyExists(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use utoipa::{Modify, OpenApi};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::{Modify, OpenApi, ToSchema};
|
||||||
|
|
||||||
use crate::api::*;
|
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).
|
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(
|
request_body(
|
||||||
content=UpdateClusterLayoutRequest,
|
content=UpdateClusterLayoutRequestOpenapi,
|
||||||
description="
|
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 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.
|
To remove a node, simply pass the `remove: true` field.
|
||||||
@@ -262,6 +263,45 @@ Contrary to the CLI that may update only a subset of the fields capacity, zone a
|
|||||||
)]
|
)]
|
||||||
fn UpdateClusterLayout() -> () {}
|
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,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Update {
|
||||||
|
/// ID of the node for which this change applies
|
||||||
|
id: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
role: NodeAssignedRole,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(post,
|
#[utoipa::path(post,
|
||||||
path = "/v2/PreviewClusterLayoutChanges",
|
path = "/v2/PreviewClusterLayoutChanges",
|
||||||
tag = "Cluster layout",
|
tag = "Cluster layout",
|
||||||
@@ -586,7 +626,7 @@ fn DenyBucketKey() -> () {}
|
|||||||
path = "/v2/AddBucketAlias",
|
path = "/v2/AddBucketAlias",
|
||||||
tag = "Bucket alias",
|
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.",
|
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(
|
responses(
|
||||||
(status = 200, description = "Returns exhaustive information about the bucket", body = AddBucketAliasResponse),
|
(status = 200, description = "Returns exhaustive information about the bucket", body = AddBucketAliasResponse),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "Internal server error")
|
||||||
@@ -598,7 +638,7 @@ fn AddBucketAlias() -> () {}
|
|||||||
path = "/v2/RemoveBucketAlias",
|
path = "/v2/RemoveBucketAlias",
|
||||||
tag = "Bucket alias",
|
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.",
|
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(
|
responses(
|
||||||
(status = 200, description = "Returns exhaustive information about the bucket", body = RemoveBucketAliasResponse),
|
(status = 200, description = "Returns exhaustive information about the bucket", body = RemoveBucketAliasResponse),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "Internal server error")
|
||||||
@@ -606,6 +646,24 @@ fn AddBucketAlias() -> () {}
|
|||||||
)]
|
)]
|
||||||
fn RemoveBucketAlias() -> () {}
|
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
|
// Node operations
|
||||||
// **********************************************
|
// **********************************************
|
||||||
@@ -818,7 +876,7 @@ impl Modify for SecurityAddon {
|
|||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
info(
|
info(
|
||||||
version = "v2.1.0",
|
version = "v2.2.0",
|
||||||
title = "Garage administration API",
|
title = "Garage administration API",
|
||||||
description = "Administrate your Garage cluster programatically, including status, layout, keys, buckets, and maintainance tasks.
|
description = "Administrate your Garage cluster programatically, including status, layout, keys, buckets, and maintainance tasks.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_api_common"
|
name = "garage_api_common"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -23,7 +23,7 @@ bytes.workspace = true
|
|||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
crc-fast.workspace = true
|
crc-fast.workspace = true
|
||||||
crypto-common.workspace = true
|
crypto-common.workspace = true
|
||||||
err-derive.workspace = true
|
thiserror.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
hmac.workspace = true
|
hmac.workspace = true
|
||||||
md-5.workspace = true
|
md-5.workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use err_derive::Error;
|
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use garage_util::error::Error as GarageError;
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
@@ -12,48 +12,48 @@ use garage_model::helper::error::Error as HelperError;
|
|||||||
pub enum CommonError {
|
pub enum CommonError {
|
||||||
// ---- INTERNAL ERRORS ----
|
// ---- INTERNAL ERRORS ----
|
||||||
/// Error related to deeper parts of Garage
|
/// Error related to deeper parts of Garage
|
||||||
#[error(display = "Internal error: {}", _0)]
|
#[error("Internal error: {0}")]
|
||||||
InternalError(#[error(source)] GarageError),
|
InternalError(#[from] GarageError),
|
||||||
|
|
||||||
/// Error related to Hyper
|
/// Error related to Hyper
|
||||||
#[error(display = "Internal error (Hyper error): {}", _0)]
|
#[error("Internal error (Hyper error): {0}")]
|
||||||
Hyper(#[error(source)] hyper::Error),
|
Hyper(#[from] hyper::Error),
|
||||||
|
|
||||||
/// Error related to HTTP
|
/// Error related to HTTP
|
||||||
#[error(display = "Internal error (HTTP error): {}", _0)]
|
#[error("Internal error (HTTP error): {0}")]
|
||||||
Http(#[error(source)] http::Error),
|
Http(#[from] http::Error),
|
||||||
|
|
||||||
// ---- GENERIC CLIENT ERRORS ----
|
// ---- GENERIC CLIENT ERRORS ----
|
||||||
/// Proper authentication was not provided
|
/// Proper authentication was not provided
|
||||||
#[error(display = "Forbidden: {}", _0)]
|
#[error("Forbidden: {0}")]
|
||||||
Forbidden(String),
|
Forbidden(String),
|
||||||
|
|
||||||
/// Generic bad request response with custom message
|
/// Generic bad request response with custom message
|
||||||
#[error(display = "Bad request: {}", _0)]
|
#[error("Bad request: {0}")]
|
||||||
BadRequest(String),
|
BadRequest(String),
|
||||||
|
|
||||||
/// The client sent a header with invalid value
|
/// The client sent a header with invalid value
|
||||||
#[error(display = "Invalid header value: {}", _0)]
|
#[error("Invalid header value: {0}")]
|
||||||
InvalidHeader(#[error(source)] hyper::header::ToStrError),
|
InvalidHeader(#[from] hyper::header::ToStrError),
|
||||||
|
|
||||||
// ---- SPECIFIC ERROR CONDITIONS ----
|
// ---- SPECIFIC ERROR CONDITIONS ----
|
||||||
// These have to be error codes referenced in the S3 spec here:
|
// These have to be error codes referenced in the S3 spec here:
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
|
||||||
/// The bucket requested don't exists
|
/// The bucket requested don't exists
|
||||||
#[error(display = "Bucket not found: {}", _0)]
|
#[error("Bucket not found: {0}")]
|
||||||
NoSuchBucket(String),
|
NoSuchBucket(String),
|
||||||
|
|
||||||
/// Tried to create a bucket that already exist
|
/// Tried to create a bucket that already exist
|
||||||
#[error(display = "Bucket already exists")]
|
#[error("Bucket already exists")]
|
||||||
BucketAlreadyExists,
|
BucketAlreadyExists,
|
||||||
|
|
||||||
/// Tried to delete a non-empty bucket
|
/// 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,
|
BucketNotEmpty,
|
||||||
|
|
||||||
// Category: bad request
|
// Category: bad request
|
||||||
/// Bucket name is not valid according to AWS S3 specs
|
/// Bucket name is not valid according to AWS S3 specs
|
||||||
#[error(display = "Invalid bucket name: {}", _0)]
|
#[error("Invalid bucket name: {0}")]
|
||||||
InvalidBucketName(String),
|
InvalidBucketName(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use err_derive::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::common_error::CommonError;
|
use crate::common_error::CommonError;
|
||||||
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
||||||
@@ -6,21 +6,21 @@ pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInterna
|
|||||||
/// Errors of this crate
|
/// Errors of this crate
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error(display = "{}", _0)]
|
#[error("{0}")]
|
||||||
/// Error from common error
|
/// Error from common error
|
||||||
Common(CommonError),
|
Common(CommonError),
|
||||||
|
|
||||||
/// Authorization Header Malformed
|
/// Authorization Header Malformed
|
||||||
#[error(display = "Authorization header malformed, unexpected scope: {}", _0)]
|
#[error("Authorization header malformed, unexpected scope: {0}")]
|
||||||
AuthorizationHeaderMalformed(String),
|
AuthorizationHeaderMalformed(String),
|
||||||
|
|
||||||
// Category: bad request
|
// Category: bad request
|
||||||
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
|
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
|
||||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
#[error("Invalid UTF-8: {0}")]
|
||||||
InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
|
InvalidUtf8Str(#[from] std::str::Utf8Error),
|
||||||
|
|
||||||
/// The provided digest (checksum) value was invalid
|
/// The provided digest (checksum) value was invalid
|
||||||
#[error(display = "Invalid digest: {}", _0)]
|
#[error("Invalid digest: {0}")]
|
||||||
InvalidDigest(String),
|
InvalidDigest(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ fn check_standard_signature(
|
|||||||
// Verify that all necessary request headers are included in signed_headers
|
// Verify that all necessary request headers are included in signed_headers
|
||||||
// The following must be included for all signatures:
|
// The following must be included for all signatures:
|
||||||
// - the Host header (mandatory)
|
// - 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
|
// 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.
|
// 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)?;
|
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
|
// Verify that all necessary request headers are included in signed_headers
|
||||||
// For AWSv4 pre-signed URLs, the following must be included:
|
// For AWSv4 pre-signed URLs, the following must be included:
|
||||||
// - the Host header (mandatory)
|
// - 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)?;
|
let signed_headers = split_signed_headers(&authorization)?;
|
||||||
verify_signed_headers(request.headers(), &signed_headers)?;
|
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"));
|
return Err(Error::bad_request("Header `Host` should be signed"));
|
||||||
}
|
}
|
||||||
for (name, _) in headers.iter() {
|
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) {
|
if !signed_headers.contains(name) {
|
||||||
return Err(Error::bad_request(format!(
|
return Err(Error::bad_request(format!(
|
||||||
"Header `{}` should be signed",
|
"Header `{}` should be signed",
|
||||||
@@ -475,8 +477,7 @@ impl Authorization {
|
|||||||
|
|
||||||
let date = headers
|
let date = headers
|
||||||
.get(X_AMZ_DATE)
|
.get(X_AMZ_DATE)
|
||||||
.ok_or_bad_request("Missing X-Amz-Date field")
|
.ok_or_bad_request("Missing X-Amz-Date field")?
|
||||||
.map_err(Error::from)?
|
|
||||||
.to_str()?;
|
.to_str()?;
|
||||||
let date = parse_date(date)?;
|
let date = parse_date(date)?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_api_k2v"
|
name = "garage_api_k2v"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -20,7 +20,7 @@ garage_util = { workspace = true, features = [ "k2v" ] }
|
|||||||
garage_api_common.workspace = true
|
garage_api_common.workspace = true
|
||||||
|
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
err-derive.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use err_derive::Error;
|
|
||||||
use hyper::header::HeaderValue;
|
use hyper::header::HeaderValue;
|
||||||
use hyper::{HeaderMap, StatusCode};
|
use hyper::{HeaderMap, StatusCode};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
pub(crate) use garage_api_common::common_error::pass_helper_error;
|
pub(crate) use garage_api_common::common_error::pass_helper_error;
|
||||||
use garage_api_common::common_error::{commonErrorDerivative, CommonError};
|
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
|
/// Errors of this crate
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error(display = "{}", _0)]
|
#[error("{0}")]
|
||||||
/// Error from common error
|
/// Error from common error
|
||||||
Common(#[error(source)] CommonError),
|
Common(#[from] CommonError),
|
||||||
|
|
||||||
// Category: cannot process
|
// Category: cannot process
|
||||||
/// Authorization Header Malformed
|
/// Authorization Header Malformed
|
||||||
#[error(display = "Authorization header malformed, unexpected scope: {}", _0)]
|
#[error("Authorization header malformed, unexpected scope: {0}")]
|
||||||
AuthorizationHeaderMalformed(String),
|
AuthorizationHeaderMalformed(String),
|
||||||
|
|
||||||
/// The provided digest (checksum) value was invalid
|
/// The provided digest (checksum) value was invalid
|
||||||
#[error(display = "Invalid digest: {}", _0)]
|
#[error("Invalid digest: {0}")]
|
||||||
InvalidDigest(String),
|
InvalidDigest(String),
|
||||||
|
|
||||||
/// The object requested don't exists
|
/// The object requested don't exists
|
||||||
#[error(display = "Key not found")]
|
#[error("Key not found")]
|
||||||
NoSuchKey,
|
NoSuchKey,
|
||||||
|
|
||||||
/// Some base64 encoded data was badly encoded
|
/// Some base64 encoded data was badly encoded
|
||||||
#[error(display = "Invalid base64: {}", _0)]
|
#[error("Invalid base64: {0}")]
|
||||||
InvalidBase64(#[error(source)] base64::DecodeError),
|
InvalidBase64(#[from] base64::DecodeError),
|
||||||
|
|
||||||
/// Invalid causality token
|
/// Invalid causality token
|
||||||
#[error(display = "Invalid causality token")]
|
#[error("Invalid causality token")]
|
||||||
InvalidCausalityToken,
|
InvalidCausalityToken,
|
||||||
|
|
||||||
/// The client asked for an invalid return format (invalid Accept header)
|
/// The client asked for an invalid return format (invalid Accept header)
|
||||||
#[error(display = "Not acceptable: {}", _0)]
|
#[error("Not acceptable: {0}")]
|
||||||
NotAcceptable(String),
|
NotAcceptable(String),
|
||||||
|
|
||||||
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
|
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
|
||||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
#[error("Invalid UTF-8: {0}")]
|
||||||
InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
|
InvalidUtf8Str(#[from] std::str::Utf8Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
commonErrorDerivative!(Error);
|
commonErrorDerivative!(Error);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_api_s3"
|
name = "garage_api_s3"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -28,7 +28,7 @@ base64.workspace = true
|
|||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
crc-fast.workspace = true
|
crc-fast.workspace = true
|
||||||
err-derive.workspace = true
|
thiserror.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
hmac.workspace = true
|
hmac.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
use err_derive::Error;
|
|
||||||
use hyper::header::HeaderValue;
|
use hyper::header::HeaderValue;
|
||||||
use hyper::{HeaderMap, StatusCode};
|
use hyper::{HeaderMap, StatusCode};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use garage_model::helper::error::Error as HelperError;
|
use garage_model::helper::error::Error as HelperError;
|
||||||
|
|
||||||
@@ -25,67 +25,67 @@ use crate::xml as s3_xml;
|
|||||||
/// Errors of this crate
|
/// Errors of this crate
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error(display = "{}", _0)]
|
#[error("{0}")]
|
||||||
/// Error from common error
|
/// Error from common error
|
||||||
Common(#[error(source)] CommonError),
|
Common(#[from] CommonError),
|
||||||
|
|
||||||
// Category: cannot process
|
// Category: cannot process
|
||||||
/// Authorization Header Malformed
|
/// Authorization Header Malformed
|
||||||
#[error(display = "Authorization header malformed, unexpected scope: {}", _0)]
|
#[error("Authorization header malformed, unexpected scope: {0}")]
|
||||||
AuthorizationHeaderMalformed(String),
|
AuthorizationHeaderMalformed(String),
|
||||||
|
|
||||||
/// The object requested don't exists
|
/// The object requested don't exists
|
||||||
#[error(display = "Key not found")]
|
#[error("Key not found")]
|
||||||
NoSuchKey,
|
NoSuchKey,
|
||||||
|
|
||||||
/// The multipart upload requested don't exists
|
/// The multipart upload requested don't exists
|
||||||
#[error(display = "Upload not found")]
|
#[error("Upload not found")]
|
||||||
NoSuchUpload,
|
NoSuchUpload,
|
||||||
|
|
||||||
/// Precondition failed (e.g. x-amz-copy-source-if-match)
|
/// 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,
|
PreconditionFailed,
|
||||||
|
|
||||||
/// Parts specified in CMU request do not match parts actually uploaded
|
/// 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,
|
InvalidPart,
|
||||||
|
|
||||||
/// Parts given to CompleteMultipartUpload were not in ascending order
|
/// 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,
|
InvalidPartOrder,
|
||||||
|
|
||||||
/// In CompleteMultipartUpload: not enough data
|
/// In CompleteMultipartUpload: not enough data
|
||||||
/// (here we are more lenient than AWS S3)
|
/// (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,
|
EntityTooSmall,
|
||||||
|
|
||||||
// Category: bad request
|
// Category: bad request
|
||||||
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
|
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
|
||||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
#[error("Invalid UTF-8: {0}")]
|
||||||
InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
|
InvalidUtf8Str(#[from] std::str::Utf8Error),
|
||||||
|
|
||||||
/// The request used an invalid path
|
/// The request used an invalid path
|
||||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
#[error("Invalid UTF-8: {0}")]
|
||||||
InvalidUtf8String(#[error(source)] std::string::FromUtf8Error),
|
InvalidUtf8String(#[from] std::string::FromUtf8Error),
|
||||||
|
|
||||||
/// The client sent invalid XML data
|
/// The client sent invalid XML data
|
||||||
#[error(display = "Invalid XML: {}", _0)]
|
#[error("Invalid XML: {0}")]
|
||||||
InvalidXml(String),
|
InvalidXml(String),
|
||||||
|
|
||||||
/// The client sent a range header with invalid value
|
/// The client sent a range header with invalid value
|
||||||
#[error(display = "Invalid HTTP range: {:?}", _0)]
|
#[error("Invalid HTTP range: {0:?}")]
|
||||||
InvalidRange(#[error(from)] (http_range::HttpRangeParseError, u64)),
|
InvalidRange((http_range::HttpRangeParseError, u64)),
|
||||||
|
|
||||||
/// The client sent a range header with invalid value
|
/// 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),
|
InvalidEncryptionAlgorithm(String),
|
||||||
|
|
||||||
/// The provided digest (checksum) value was invalid
|
/// The provided digest (checksum) value was invalid
|
||||||
#[error(display = "Invalid digest: {}", _0)]
|
#[error("Invalid digest: {0}")]
|
||||||
InvalidDigest(String),
|
InvalidDigest(String),
|
||||||
|
|
||||||
/// The client sent a request for an action not supported by garage
|
/// The client sent a request for an action not supported by garage
|
||||||
#[error(display = "Unimplemented action: {}", _0)]
|
#[error("Unimplemented action: {0}")]
|
||||||
NotImplemented(String),
|
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 {
|
impl From<roxmltree::Error> for Error {
|
||||||
fn from(err: roxmltree::Error) -> Self {
|
fn from(err: roxmltree::Error) -> Self {
|
||||||
Self::InvalidXml(format!("{}", err))
|
Self::InvalidXml(format!("{}", err))
|
||||||
|
|||||||
@@ -853,7 +853,9 @@ impl PreconditionHeaders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check(&self, v: &ObjectVersion, etag: &str) -> Result<Option<StatusCode>, Error> {
|
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
|
// 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()?;
|
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() {
|
for (param_key, value) in params.iter() {
|
||||||
let param_key = param_key.as_str();
|
let param_key = param_key.as_str();
|
||||||
match param_key {
|
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" => {
|
"content-type" => {
|
||||||
let conds = conditions.params.remove("content-type").ok_or_else(|| {
|
let conds = conditions.params.remove("content-type").ok_or_else(|| {
|
||||||
Error::bad_request(format!("Key '{}' is not allowed in policy", param_key))
|
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::error::*;
|
||||||
use crate::website::X_AMZ_WEBSITE_REDIRECT_LOCATION;
|
use crate::website::X_AMZ_WEBSITE_REDIRECT_LOCATION;
|
||||||
|
|
||||||
const PUT_BLOCKS_MAX_PARALLEL: usize = 3;
|
|
||||||
|
|
||||||
pub(crate) struct SaveStreamResult {
|
pub(crate) struct SaveStreamResult {
|
||||||
pub(crate) version_uuid: Uuid,
|
pub(crate) version_uuid: Uuid,
|
||||||
pub(crate) version_timestamp: u64,
|
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 {
|
let recv_next = async {
|
||||||
// If more than a maximum number of writes are in progress, don't add more for now
|
// 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
|
futures::future::pending().await
|
||||||
} else {
|
} else {
|
||||||
block_rx3.recv().await
|
block_rx3.recv().await
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_block"
|
name = "garage_block"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_db"
|
name = "garage_db"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -12,7 +12,7 @@ readme = "../../README.md"
|
|||||||
path = "lib.rs"
|
path = "lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
err-derive.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|
||||||
heed = { workspace = true, optional = true }
|
heed = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use std::cell::Cell;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use err_derive::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub use open::*;
|
pub use open::*;
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ pub type TxValueIter<'a> = Box<dyn std::iter::Iterator<Item = TxOpResult<(Value,
|
|||||||
// ----
|
// ----
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
#[error(display = "{}", _0)]
|
#[error("{0}")]
|
||||||
pub struct Error(pub Cow<'static, str>);
|
pub struct Error(pub Cow<'static, str>);
|
||||||
|
|
||||||
impl From<std::io::Error> for Error {
|
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>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
#[error(display = "{}", _0)]
|
#[error("{0}")]
|
||||||
pub struct TxOpError(pub(crate) Error);
|
pub struct TxOpError(pub(crate) Error);
|
||||||
pub type TxOpResult<T> = std::result::Result<T, TxOpError>;
|
pub type TxOpResult<T> = std::result::Result<T, TxOpError>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage"
|
name = "garage"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
@@ -68,24 +68,30 @@ struct Opt {
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
// Initialize version and features info
|
// Initialize version and features info
|
||||||
let features = &[
|
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")]
|
#[cfg(feature = "bundled-libs")]
|
||||||
"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")]
|
#[cfg(feature = "system-libs")]
|
||||||
"system-libs",
|
"system-libs",
|
||||||
|
#[cfg(feature = "telemetry-otlp")]
|
||||||
|
"telemetry-otlp",
|
||||||
][..];
|
][..];
|
||||||
if let Some(git_version) = option_env!("GIT_VERSION") {
|
if let Some(git_version) = option_env!("GIT_VERSION") {
|
||||||
garage_util::version::init_version(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 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 newer_date = DateTime::from_secs_f64(last_modified.as_secs_f64() + 10.0);
|
||||||
{
|
{
|
||||||
let err = ctx
|
let err = ctx
|
||||||
@@ -212,6 +213,18 @@ async fn test_precondition() {
|
|||||||
matches!(err, Err(SdkError::ServiceError(se)) if se.raw().status().as_u16() == 304)
|
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
|
let o = ctx
|
||||||
.client
|
.client
|
||||||
.get_object()
|
.get_object()
|
||||||
@@ -236,6 +249,17 @@ async fn test_precondition() {
|
|||||||
matches!(err, Err(SdkError::ServiceError(se)) if se.raw().status().as_u16() == 412)
|
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
|
let o = ctx
|
||||||
.client
|
.client
|
||||||
.get_object()
|
.get_object()
|
||||||
|
|||||||
@@ -573,24 +573,68 @@ async fn test_website_redirect_full_bucket() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let req = Request::builder()
|
{
|
||||||
.method("GET")
|
let req = Request::builder()
|
||||||
.uri(format!("http://127.0.0.1:{}/my-path", ctx.garage.web_port))
|
.method("GET")
|
||||||
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
.uri(format!("http://127.0.0.1:{}/my-path", ctx.garage.web_port))
|
||||||
.body(Body::new(Bytes::new()))
|
.header("Host", format!("{}.web.garage", BCKT_NAME))
|
||||||
.unwrap();
|
.body(Body::new(Bytes::new()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let client = Client::builder(TokioExecutor::new()).build_http();
|
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||||
let resp = client.request(req).await.unwrap();
|
let resp = client.request(req).await.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers()
|
resp.headers()
|
||||||
.get(hyper::header::LOCATION)
|
.get(hyper::header::LOCATION)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
"https://other.tld/my-path"
|
"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]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_model"
|
name = "garage_model"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -25,7 +25,7 @@ argon2.workspace = true
|
|||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
blake2.workspace = true
|
blake2.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
err-derive.workspace = true
|
thiserror.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
|
|||||||
@@ -321,15 +321,15 @@ impl Garage {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bucket_helper(&self) -> helper::bucket::BucketHelper {
|
pub fn bucket_helper(&self) -> helper::bucket::BucketHelper<'_> {
|
||||||
helper::bucket::BucketHelper(self)
|
helper::bucket::BucketHelper(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn key_helper(&self) -> helper::key::KeyHelper {
|
pub fn key_helper(&self) -> helper::key::KeyHelper<'_> {
|
||||||
helper::key::KeyHelper(self)
|
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;
|
let lock = self.bucket_lock.lock().await;
|
||||||
helper::locked::LockedHelper(self, Some(lock))
|
helper::locked::LockedHelper(self, Some(lock))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
use err_derive::Error;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use garage_util::error::Error as GarageError;
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
#[derive(Debug, Error, Serialize, Deserialize)]
|
#[derive(Debug, Error, Serialize, Deserialize)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error(display = "Internal error: {}", _0)]
|
#[error("Internal error: {0}")]
|
||||||
Internal(#[error(source)] GarageError),
|
Internal(#[from] GarageError),
|
||||||
|
|
||||||
#[error(display = "Bad request: {}", _0)]
|
#[error("Bad request: {0}")]
|
||||||
BadRequest(String),
|
BadRequest(String),
|
||||||
|
|
||||||
/// Bucket name is not valid according to AWS S3 specs
|
/// Bucket name is not valid according to AWS S3 specs
|
||||||
#[error(display = "Invalid bucket name: {}", _0)]
|
#[error("Invalid bucket name: {0}")]
|
||||||
InvalidBucketName(String),
|
InvalidBucketName(String),
|
||||||
|
|
||||||
#[error(display = "Access key not found: {}", _0)]
|
#[error("Access key not found: {0}")]
|
||||||
NoSuchAccessKey(String),
|
NoSuchAccessKey(String),
|
||||||
|
|
||||||
#[error(display = "Bucket not found: {}", _0)]
|
#[error("Bucket not found: {0}")]
|
||||||
NoSuchBucket(String),
|
NoSuchBucket(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_net"
|
name = "garage_net"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -30,7 +30,7 @@ rand.workspace = true
|
|||||||
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
arc-swap.workspace = true
|
arc-swap.workspace = true
|
||||||
err-derive.workspace = true
|
thiserror.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
cfg-if.workspace = true
|
cfg-if.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ where
|
|||||||
pub(crate) type DynEndpoint = Box<dyn GenericEndpoint + Send + Sync>;
|
pub(crate) type DynEndpoint = Box<dyn GenericEndpoint + Send + Sync>;
|
||||||
|
|
||||||
pub(crate) trait GenericEndpoint {
|
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 drop_handler(&self);
|
||||||
fn clone_endpoint(&self) -> DynEndpoint;
|
fn clone_endpoint(&self) -> DynEndpoint;
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ where
|
|||||||
M: Message,
|
M: Message,
|
||||||
H: StreamingEndpointHandler<M> + 'static,
|
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 {
|
async move {
|
||||||
match self.0.handler.load_full() {
|
match self.0.handler.load_full() {
|
||||||
None => Err(Error::NoHandler),
|
None => Err(Error::NoHandler),
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use err_derive::Error;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error(display = "IO error: {}", _0)]
|
#[error("IO error: {0}")]
|
||||||
Io(#[error(source)] io::Error),
|
Io(#[from] io::Error),
|
||||||
|
|
||||||
#[error(display = "Messagepack encode error: {}", _0)]
|
#[error("Messagepack encode error: {0}")]
|
||||||
RMPEncode(#[error(source)] rmp_serde::encode::Error),
|
RMPEncode(#[from] rmp_serde::encode::Error),
|
||||||
#[error(display = "Messagepack decode error: {}", _0)]
|
#[error("Messagepack decode error: {0}")]
|
||||||
RMPDecode(#[error(source)] rmp_serde::decode::Error),
|
RMPDecode(#[from] rmp_serde::decode::Error),
|
||||||
|
|
||||||
#[error(display = "Tokio join error: {}", _0)]
|
#[error("Tokio join error: {0}")]
|
||||||
TokioJoin(#[error(source)] tokio::task::JoinError),
|
TokioJoin(#[from] tokio::task::JoinError),
|
||||||
|
|
||||||
#[error(display = "oneshot receive error: {}", _0)]
|
#[error("oneshot receive error: {0}")]
|
||||||
OneshotRecv(#[error(source)] tokio::sync::oneshot::error::RecvError),
|
OneshotRecv(#[from] tokio::sync::oneshot::error::RecvError),
|
||||||
|
|
||||||
#[error(display = "Handshake error: {}", _0)]
|
#[error("Handshake error: {0}")]
|
||||||
Handshake(#[error(source)] kuska_handshake::async_std::Error),
|
Handshake(#[from] kuska_handshake::async_std::Error),
|
||||||
|
|
||||||
#[error(display = "UTF8 error: {}", _0)]
|
#[error("UTF8 error: {0}")]
|
||||||
UTF8(#[error(source)] std::string::FromUtf8Error),
|
UTF8(#[from] std::string::FromUtf8Error),
|
||||||
|
|
||||||
#[error(display = "Framing protocol error")]
|
#[error("Framing protocol error")]
|
||||||
Framing,
|
Framing,
|
||||||
|
|
||||||
#[error(display = "Remote error ({:?}): {}", _0, _1)]
|
#[error("Remote error ({0:?}): {1}")]
|
||||||
Remote(io::ErrorKind, String),
|
Remote(io::ErrorKind, String),
|
||||||
|
|
||||||
#[error(display = "Request ID collision")]
|
#[error("Request ID collision")]
|
||||||
IdCollision,
|
IdCollision,
|
||||||
|
|
||||||
#[error(display = "{}", _0)]
|
#[error("{0}")]
|
||||||
Message(String),
|
Message(String),
|
||||||
|
|
||||||
#[error(display = "No handler / shutting down")]
|
#[error("No handler / shutting down")]
|
||||||
NoHandler,
|
NoHandler,
|
||||||
|
|
||||||
#[error(display = "Connection closed")]
|
#[error("Connection closed")]
|
||||||
ConnectionClosed,
|
ConnectionClosed,
|
||||||
|
|
||||||
#[error(display = "Version mismatch: {}", _0)]
|
#[error("Version mismatch: {0}")]
|
||||||
VersionMismatch(String),
|
VersionMismatch(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_rpc"
|
name = "garage_rpc"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -33,7 +33,7 @@ async-trait.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_bytes.workspace = true
|
serde_bytes.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
err-derive = { workspace = true, optional = true }
|
thiserror = { workspace = true, optional = true }
|
||||||
|
|
||||||
# newer version requires rust edition 2021
|
# newer version requires rust edition 2021
|
||||||
kube = { workspace = true, optional = true }
|
kube = { workspace = true, optional = true }
|
||||||
@@ -49,5 +49,5 @@ opentelemetry.workspace = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
kubernetes-discovery = [ "kube", "k8s-openapi", "schemars" ]
|
kubernetes-discovery = [ "kube", "k8s-openapi", "schemars" ]
|
||||||
consul-discovery = [ "reqwest", "err-derive" ]
|
consul-discovery = [ "reqwest", "thiserror" ]
|
||||||
system-libs = [ "sodiumoxide/use-pkg-config" ]
|
system-libs = [ "sodiumoxide/use-pkg-config" ]
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use std::fs::File;
|
|||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
|
||||||
use err_derive::Error;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use garage_net::NodeID;
|
use garage_net::NodeID;
|
||||||
|
|
||||||
@@ -126,38 +126,61 @@ impl ConsulDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- READING FROM CONSUL CATALOG ----
|
// ---- 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> {
|
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![];
|
let mut ret = vec![];
|
||||||
for ent in entries {
|
|
||||||
let ip = ent.address.parse::<IpAddr>().ok();
|
let dcs_to_query: Vec<Option<&str>> = match &self.config.datacenters {
|
||||||
let pubkey = ent
|
dcs if !dcs.is_empty() => dcs.iter().map(|dc| Some(dc.as_str())).collect(),
|
||||||
.meta
|
_ => vec![None],
|
||||||
.get(&format!("{}-pubkey", META_PREFIX))
|
};
|
||||||
.and_then(|k| hex::decode(k).ok())
|
|
||||||
.and_then(|k| NodeID::from_slice(&k[..]));
|
for dc in dcs_to_query {
|
||||||
if let (Some(ip), Some(pubkey)) = (ip, pubkey) {
|
let url = match dc {
|
||||||
ret.push((pubkey, SocketAddr::new(ip, ent.service_port)));
|
Some(datacenter) => format!(
|
||||||
} else {
|
"{}/v1/catalog/service/{}?dc={}",
|
||||||
warn!(
|
self.config.consul_http_addr, self.config.service_name, datacenter
|
||||||
"Could not process node spec from Consul: {:?} (invalid IP address or node ID/pubkey)",
|
),
|
||||||
ent
|
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)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- PUBLISHING TO CONSUL CATALOG ----
|
// ---- PUBLISHING TO CONSUL CATALOG ----
|
||||||
|
|
||||||
pub async fn publish_consul_service(
|
pub async fn publish_consul_service(
|
||||||
@@ -219,12 +242,12 @@ impl ConsulDiscovery {
|
|||||||
/// Regroup all Consul discovery errors
|
/// Regroup all Consul discovery errors
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ConsulError {
|
pub enum ConsulError {
|
||||||
#[error(display = "IO error: {}", _0)]
|
#[error("IO error: {0}")]
|
||||||
Io(#[error(source)] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error(display = "HTTP error: {}", _0)]
|
#[error("HTTP error: {0}")]
|
||||||
Reqwest(#[error(source)] reqwest::Error),
|
Reqwest(#[from] reqwest::Error),
|
||||||
#[error(display = "Invalid Consul TLS configuration")]
|
#[error("Invalid Consul TLS configuration")]
|
||||||
InvalidTLSConfig,
|
InvalidTLSConfig,
|
||||||
#[error(display = "Token error: {}", _0)]
|
#[error("Token error: {0}")]
|
||||||
Token(#[error(source)] reqwest::header::InvalidHeaderValue),
|
Token(#[from] reqwest::header::InvalidHeaderValue),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -517,7 +517,7 @@ impl LayoutVersion {
|
|||||||
g.compute_maximal_flow()?;
|
g.compute_maximal_flow()?;
|
||||||
if g.get_flow_value()? < (NB_PARTITIONS * self.replication_factor) as i64 {
|
if g.get_flow_value()? < (NB_PARTITIONS * self.replication_factor) as i64 {
|
||||||
return Err(Error::Message(
|
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."
|
impossible to store partitions of size 1."
|
||||||
.into(),
|
.into(),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_table"
|
name = "garage_table"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_util"
|
name = "garage_util"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -21,7 +21,7 @@ arc-swap.workspace = true
|
|||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
blake2.workspace = true
|
blake2.workspace = true
|
||||||
bytesize.workspace = true
|
bytesize.workspace = true
|
||||||
err-derive.workspace = true
|
thiserror.workspace = true
|
||||||
hexdump.workspace = true
|
hexdump.workspace = true
|
||||||
xxhash-rust.workspace = true
|
xxhash-rust.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ pub struct Config {
|
|||||||
)]
|
)]
|
||||||
pub block_size: usize,
|
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.
|
/// Number of replicas. Can be any positive integer, but uneven numbers are more favorable.
|
||||||
/// - 1 for single-node clusters, or to disable replication
|
/// - 1 for single-node clusters, or to disable replication
|
||||||
/// - 3 is the recommended and supported setting.
|
/// - 3 is the recommended and supported setting.
|
||||||
@@ -257,6 +262,8 @@ pub struct ConsulDiscoveryConfig {
|
|||||||
/// Additional service metadata to add
|
/// Additional service metadata to add
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub meta: Option<std::collections::HashMap<String, String>>,
|
pub meta: Option<std::collections::HashMap<String, String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub datacenters: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
@@ -270,6 +277,9 @@ pub struct KubernetesDiscoveryConfig {
|
|||||||
pub skip_crd: bool,
|
pub skip_crd: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_block_max_concurrent_writes_per_request() -> usize {
|
||||||
|
3
|
||||||
|
}
|
||||||
/// Read and parse configuration
|
/// Read and parse configuration
|
||||||
pub fn read_config(config_file: PathBuf) -> Result<Config, Error> {
|
pub fn read_config(config_file: PathBuf) -> Result<Config, Error> {
|
||||||
let config = std::fs::read_to_string(config_file)?;
|
let config = std::fs::read_to_string(config_file)?;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use err_derive::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
@@ -12,71 +12,64 @@ use crate::encode::debug_serialize;
|
|||||||
/// Regroup all Garage errors
|
/// Regroup all Garage errors
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error(display = "IO error: {}", _0)]
|
#[error("IO error: {0}")]
|
||||||
Io(#[error(source)] io::Error),
|
Io(#[from] io::Error),
|
||||||
|
|
||||||
#[error(display = "Hyper error: {}", _0)]
|
#[error("Hyper error: {0}")]
|
||||||
Hyper(#[error(source)] hyper::Error),
|
Hyper(#[from] hyper::Error),
|
||||||
|
|
||||||
#[error(display = "HTTP error: {}", _0)]
|
#[error("HTTP error: {0}")]
|
||||||
Http(#[error(source)] http::Error),
|
Http(#[from] http::Error),
|
||||||
|
|
||||||
#[error(display = "Invalid HTTP header value: {}", _0)]
|
#[error("Invalid HTTP header value: {0}")]
|
||||||
HttpHeader(#[error(source)] http::header::ToStrError),
|
HttpHeader(#[from] http::header::ToStrError),
|
||||||
|
|
||||||
#[error(display = "Network error: {}", _0)]
|
#[error("Network error: {0}")]
|
||||||
Net(#[error(source)] garage_net::error::Error),
|
Net(#[from] garage_net::error::Error),
|
||||||
|
|
||||||
#[error(display = "DB error: {}", _0)]
|
#[error("DB error: {0}")]
|
||||||
Db(#[error(source)] garage_db::Error),
|
Db(#[from] garage_db::Error),
|
||||||
|
|
||||||
#[error(display = "Messagepack encode error: {}", _0)]
|
#[error("Messagepack encode error: {0}")]
|
||||||
RmpEncode(#[error(source)] rmp_serde::encode::Error),
|
RmpEncode(#[from] rmp_serde::encode::Error),
|
||||||
#[error(display = "Messagepack decode error: {}", _0)]
|
#[error("Messagepack decode error: {0}")]
|
||||||
RmpDecode(#[error(source)] rmp_serde::decode::Error),
|
RmpDecode(#[from] rmp_serde::decode::Error),
|
||||||
#[error(display = "JSON error: {}", _0)]
|
#[error("JSON error: {0}")]
|
||||||
Json(#[error(source)] serde_json::error::Error),
|
Json(#[from] serde_json::error::Error),
|
||||||
#[error(display = "TOML decode error: {}", _0)]
|
#[error("TOML decode error: {0}")]
|
||||||
TomlDecode(#[error(source)] toml::de::Error),
|
TomlDecode(#[from] toml::de::Error),
|
||||||
|
|
||||||
#[error(display = "Tokio join error: {}", _0)]
|
#[error("Tokio join error: {0}")]
|
||||||
TokioJoin(#[error(source)] tokio::task::JoinError),
|
TokioJoin(#[from] tokio::task::JoinError),
|
||||||
|
|
||||||
#[error(display = "Tokio semaphore acquire error: {}", _0)]
|
#[error("Tokio semaphore acquire error: {0}")]
|
||||||
TokioSemAcquire(#[error(source)] tokio::sync::AcquireError),
|
TokioSemAcquire(#[from] tokio::sync::AcquireError),
|
||||||
|
|
||||||
#[error(display = "Tokio broadcast receive error: {}", _0)]
|
#[error("Tokio broadcast receive error: {0}")]
|
||||||
TokioBcastRecv(#[error(source)] tokio::sync::broadcast::error::RecvError),
|
TokioBcastRecv(#[from] tokio::sync::broadcast::error::RecvError),
|
||||||
|
|
||||||
#[error(display = "Remote error: {}", _0)]
|
#[error("Remote error: {0}")]
|
||||||
RemoteError(String),
|
RemoteError(String),
|
||||||
|
|
||||||
#[error(display = "Timeout")]
|
#[error("Timeout")]
|
||||||
Timeout,
|
Timeout,
|
||||||
|
|
||||||
#[error(display = "Layout not ready")]
|
#[error("Layout not ready")]
|
||||||
LayoutNotReady,
|
LayoutNotReady,
|
||||||
|
|
||||||
#[error(
|
#[error("Could not reach quorum of {0} (sets={1:?}). {2} of {3} request succeeded, others returned errors: {4:?}")]
|
||||||
display = "Could not reach quorum of {} (sets={:?}). {} of {} request succeeded, others returned errors: {:?}",
|
|
||||||
_0,
|
|
||||||
_1,
|
|
||||||
_2,
|
|
||||||
_3,
|
|
||||||
_4
|
|
||||||
)]
|
|
||||||
Quorum(usize, Option<usize>, usize, usize, Vec<String>),
|
Quorum(usize, Option<usize>, usize, usize, Vec<String>),
|
||||||
|
|
||||||
#[error(display = "Unexpected RPC message: {}", _0)]
|
#[error("Unexpected RPC message: {0}")]
|
||||||
UnexpectedRpcMessage(String),
|
UnexpectedRpcMessage(String),
|
||||||
|
|
||||||
#[error(display = "Corrupt data: does not match hash {:?}", _0)]
|
#[error("Corrupt data: does not match hash {0:?}")]
|
||||||
CorruptData(Hash),
|
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),
|
MissingBlock(Hash),
|
||||||
|
|
||||||
#[error(display = "{}", _0)]
|
#[error("{0}")]
|
||||||
Message(String),
|
Message(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_web"
|
name = "garage_web"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -20,7 +20,7 @@ garage_model.workspace = true
|
|||||||
garage_util.workspace = true
|
garage_util.workspace = true
|
||||||
garage_table.workspace = true
|
garage_table.workspace = true
|
||||||
|
|
||||||
err-derive.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
percent-encoding.workspace = true
|
percent-encoding.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use err_derive::Error;
|
|
||||||
use hyper::header::HeaderValue;
|
use hyper::header::HeaderValue;
|
||||||
use hyper::{HeaderMap, StatusCode};
|
use hyper::{HeaderMap, StatusCode};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use garage_api_common::generic_server::ApiError;
|
use garage_api_common::generic_server::ApiError;
|
||||||
|
|
||||||
@@ -8,15 +8,15 @@ use garage_api_common::generic_server::ApiError;
|
|||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// An error received from the API crate
|
/// An error received from the API crate
|
||||||
#[error(display = "API error: {}", _0)]
|
#[error("API error: {0}")]
|
||||||
ApiError(garage_api_s3::error::Error),
|
ApiError(garage_api_s3::error::Error),
|
||||||
|
|
||||||
/// The file does not exist
|
/// The file does not exist
|
||||||
#[error(display = "Not found")]
|
#[error("Not found")]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
||||||
/// The client sent a request without host, or with unsupported method
|
/// The client sent a request without host, or with unsupported method
|
||||||
#[error(display = "Bad request: {}", _0)]
|
#[error("Bad request: {0}")]
|
||||||
BadRequest(String),
|
BadRequest(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -579,7 +579,7 @@ fn path_to_keys(
|
|||||||
};
|
};
|
||||||
if let Some(condition) = &routing_rule.condition {
|
if let Some(condition) = &routing_rule.condition {
|
||||||
let suffix = if let Some(prefix) = &condition.prefix {
|
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;
|
continue;
|
||||||
};
|
};
|
||||||
Some(suffix)
|
Some(suffix)
|
||||||
|
|||||||