mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-08-16 00:43:11 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a4afc04a9 | |||
| fbf03e9378 | |||
| 9eb07d4c7b | |||
| 85ee4f5d8c | |||
| 328072d122 | |||
| 26bc807905 | |||
| a9f5f242b2 | |||
| ae98abca5c | |||
| adfa44ad70 | |||
| 47143b88ad | |||
| 8843aa92fa | |||
| b601b3e46d | |||
| a19d2f16e2 | |||
| fc8fc60f6d | |||
| 77079a1498 | |||
| 2a4f729b57 | |||
| 1b042e379e | |||
| ffbce0f689 | |||
| 37e5621dde | |||
| 6529ff379a | |||
| a8d73682a4 | |||
| 8654eb19bf | |||
| 54ea412188 | |||
| 2ade8c86f6 | |||
| b15e2cbb6c | |||
| 0fd1b7342b | |||
| be16bc7a05 | |||
| bfaa1ca6b7 | |||
| de8eeab4ad | |||
| ae3f7ee76c | |||
| 2dc3a6dbbe | |||
| c6bc3f229b | |||
| bba9202f31 | |||
| a605a80806 | |||
| 539af12d21 | |||
| a2a9e3cec4 | |||
| 14274bc13c |
Generated
+28
-58
@@ -867,9 +867,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.14"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
@@ -1200,7 +1200,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"async-trait",
|
||||
@@ -1250,12 +1250,13 @@ dependencies = [
|
||||
"timeago",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-journald",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garage_api_admin"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"async-trait",
|
||||
@@ -1281,7 +1282,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_api_common"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
@@ -1300,7 +1301,6 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"idna 0.5.0",
|
||||
"md-5",
|
||||
"nom",
|
||||
"opentelemetry",
|
||||
@@ -1316,7 +1316,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_api_k2v"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"err-derive",
|
||||
@@ -1339,7 +1339,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_api_s3"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"async-compression",
|
||||
@@ -1384,7 +1384,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_block"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-compression",
|
||||
@@ -1409,7 +1409,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_db"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"err-derive",
|
||||
"heed",
|
||||
@@ -1422,7 +1422,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_model"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.21.7",
|
||||
@@ -1449,7 +1449,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_net"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"bytes",
|
||||
@@ -1470,12 +1470,11 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util 0.7.14",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garage_rpc"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
@@ -1507,7 +1506,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_table"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
@@ -1528,7 +1527,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_util"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
@@ -1560,7 +1559,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_web"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"err-derive",
|
||||
"garage_api_common",
|
||||
@@ -2171,16 +2170,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
@@ -4253,21 +4242,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.44.1"
|
||||
@@ -4541,6 +4515,17 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-journald"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0b4143302cf1022dac868d521e36e8b27691f72c84b3311750d5188ebba657"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
@@ -4588,27 +4573,12 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
@@ -4656,7 +4626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna 1.0.3",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
|
||||
+13
-13
@@ -24,18 +24,18 @@ default-members = ["src/garage"]
|
||||
|
||||
# Internal Garage crates
|
||||
format_table = { version = "0.1.1", path = "src/format-table" }
|
||||
garage_api_common = { version = "1.1.0", path = "src/api/common" }
|
||||
garage_api_admin = { version = "1.1.0", path = "src/api/admin" }
|
||||
garage_api_s3 = { version = "1.1.0", path = "src/api/s3" }
|
||||
garage_api_k2v = { version = "1.1.0", path = "src/api/k2v" }
|
||||
garage_block = { version = "1.1.0", path = "src/block" }
|
||||
garage_db = { version = "1.1.0", path = "src/db", default-features = false }
|
||||
garage_model = { version = "1.1.0", path = "src/model", default-features = false }
|
||||
garage_net = { version = "1.1.0", path = "src/net" }
|
||||
garage_rpc = { version = "1.1.0", path = "src/rpc" }
|
||||
garage_table = { version = "1.1.0", path = "src/table" }
|
||||
garage_util = { version = "1.1.0", path = "src/util" }
|
||||
garage_web = { version = "1.1.0", path = "src/web" }
|
||||
garage_api_common = { version = "1.2.0", path = "src/api/common" }
|
||||
garage_api_admin = { version = "1.2.0", path = "src/api/admin" }
|
||||
garage_api_s3 = { version = "1.2.0", path = "src/api/s3" }
|
||||
garage_api_k2v = { version = "1.2.0", path = "src/api/k2v" }
|
||||
garage_block = { version = "1.2.0", path = "src/block" }
|
||||
garage_db = { version = "1.2.0", path = "src/db", default-features = false }
|
||||
garage_model = { version = "1.2.0", path = "src/model", default-features = false }
|
||||
garage_net = { version = "1.2.0", path = "src/net" }
|
||||
garage_rpc = { version = "1.2.0", path = "src/rpc" }
|
||||
garage_table = { version = "1.2.0", path = "src/table" }
|
||||
garage_util = { version = "1.2.0", path = "src/util" }
|
||||
garage_web = { version = "1.2.0", path = "src/web" }
|
||||
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
|
||||
|
||||
# External crates from crates.io
|
||||
@@ -58,7 +58,6 @@ git-version = "0.3.4"
|
||||
hex = "0.4"
|
||||
hexdump = "0.1"
|
||||
hmac = "0.12"
|
||||
idna = "0.5"
|
||||
itertools = "0.12"
|
||||
ipnet = "2.9.0"
|
||||
lazy_static = "1.4"
|
||||
@@ -84,6 +83,7 @@ pretty_env_logger = "0.5"
|
||||
structopt = { version = "0.3", default-features = false }
|
||||
syslog-tracing = "0.3"
|
||||
tracing = "0.1"
|
||||
tracing-journald = "0.3.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
heed = { version = "0.11", default-features = false, features = ["lmdb"] }
|
||||
|
||||
@@ -8,18 +8,18 @@ have published Ansible roles. We list them and compare them below.
|
||||
|
||||
## Comparison of Ansible roles
|
||||
|
||||
| Feature | [ansible-role-garage](#zorun-ansible-role-garage) | [garage-docker-ansible-deploy](#moan0s-garage-docker-ansible-deploy) |
|
||||
|------------------------------------|---------------------------------------------|---------------------------------------------------------------|
|
||||
| **Runtime** | Systemd | Docker |
|
||||
| **Target OS** | Any Linux | Any Linux |
|
||||
| **Architecture** | amd64, arm64, i686 | amd64, arm64 |
|
||||
| **Additional software** | None | Traefik |
|
||||
| **Automatic node connection** | ❌ | ✅ |
|
||||
| **Layout management** | ❌ | ✅ |
|
||||
| **Manage buckets & keys** | ❌ | ✅ (basic) |
|
||||
| **Allow custom Garage config** | ✅ | ❌ |
|
||||
| **Facilitate Garage upgrades** | ✅ | ❌ |
|
||||
| **Multiple instances on one host** | ✅ | ✅ |
|
||||
| Feature | [ansible-role-garage](#zorun-ansible-role-garage) | [garage-docker-ansible-deploy](#moan0s-garage-docker-ansible-deploy) | [eddster ansible-role-garage](#eddster-ansible-role-garage) |
|
||||
|------------------------------------|---------------------------------------------|---------------------------------------------------------------|---------------------------------|
|
||||
| **Runtime** | Systemd | Docker | Systemd |
|
||||
| **Target OS** | Any Linux | Any Linux | Any Linux |
|
||||
| **Architecture** | amd64, arm64, i686 | amd64, arm64 | arm64, arm, 386, amd64 |
|
||||
| **Additional software** | None | Traefik | Ngnix and Keepalived (optional) |
|
||||
| **Automatic node connection** | ❌ | ✅ | ✅ |
|
||||
| **Layout management** | ❌ | ✅ | ✅ |
|
||||
| **Manage buckets & keys** | ❌ | ✅ (basic) | ✅ |
|
||||
| **Allow custom Garage config** | ✅ | ❌ | ❌ |
|
||||
| **Facilitate Garage upgrades** | ✅ | ❌ | ✅ |
|
||||
| **Multiple instances on one host** | ✅ | ✅ | ❌ |
|
||||
|
||||
|
||||
## zorun/ansible-role-garage
|
||||
@@ -49,3 +49,15 @@ structured DNS names, etc).
|
||||
|
||||
As a result, this role makes it easier to start with Garage on Ansible,
|
||||
but is less flexible.
|
||||
|
||||
## eddster2309/ansible-role-garage
|
||||
|
||||
[Source code](https://github.com/eddster2309/ansible-role-garage), [Ansible galaxy](https://galaxy.ansible.com/ui/standalone/roles/eddster2309/garage/)
|
||||
|
||||
This role is a opinionated but customisable role using the official Garage
|
||||
static binaries and only requires Systemd. As such it should work on any
|
||||
Linux based host. It includes all the nesscary configuration to
|
||||
automatically setup a clustered Garage deployment. Most Garage
|
||||
configuration options are exposed through Ansible variables so while you
|
||||
can't provide a custom config you can get very close. It can optionally
|
||||
installed a HA nginx deployment with Keepalived.
|
||||
|
||||
@@ -26,6 +26,13 @@ Or deploy with custom values:
|
||||
helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml
|
||||
```
|
||||
|
||||
If you want to manage the CustomRessourceDefinition used by garage for its `kubernetes_discovery` outside of the helm chart, add `garage.kubernetesSkipCrd: true` to your custom values and use the kustomization before deploying the helm chart:
|
||||
|
||||
```bash
|
||||
kubectl apply -k ../k8s/crd
|
||||
helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml
|
||||
```
|
||||
|
||||
After deploying, cluster layout must be configured manually as described in [Creating a cluster layout](@/documentation/quick-start/_index.md#creating-a-cluster-layout). Use the following command to access garage CLI:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -96,14 +96,14 @@ to store 2 TB of data in total.
|
||||
## Get a Docker image
|
||||
|
||||
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
||||
We encourage you to use a fixed tag (eg. `v1.1.0`) and not the `latest` tag.
|
||||
For this example, we will use the latest published version at the time of the writing which is `v1.1.0` but it's up to you
|
||||
We encourage you to use a fixed tag (eg. `v1.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 `v1.2.0` but it's up to you
|
||||
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
sudo docker pull dxflrs/garage:v1.1.0
|
||||
sudo docker pull dxflrs/garage:v1.2.0
|
||||
```
|
||||
|
||||
## Deploying and configuring Garage
|
||||
@@ -171,7 +171,7 @@ docker run \
|
||||
-v /etc/garage.toml:/etc/garage.toml \
|
||||
-v /var/lib/garage/meta:/var/lib/garage/meta \
|
||||
-v /var/lib/garage/data:/var/lib/garage/data \
|
||||
dxflrs/garage:v1.1.0
|
||||
dxflrs/garage:v1.2.0
|
||||
```
|
||||
|
||||
With this command line, Garage should be started automatically at each boot.
|
||||
@@ -185,7 +185,7 @@ If you want to use `docker-compose`, you may use the following `docker-compose.y
|
||||
version: "3"
|
||||
services:
|
||||
garage:
|
||||
image: dxflrs/garage:v1.1.0
|
||||
image: dxflrs/garage:v1.2.0
|
||||
network_mode: "host"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
||||
@@ -28,6 +28,7 @@ StateDirectory=garage
|
||||
DynamicUser=true
|
||||
ProtectHome=true
|
||||
NoNewPrivileges=true
|
||||
LimitNOFILE=42000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -132,7 +132,7 @@ docker run \
|
||||
-v /path/to/garage.toml:/etc/garage.toml \
|
||||
-v /path/to/garage/meta:/var/lib/garage/meta \
|
||||
-v /path/to/garage/data:/var/lib/garage/data \
|
||||
dxflrs/garage:v1.1.0
|
||||
dxflrs/garage:v1.2.0
|
||||
```
|
||||
|
||||
Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`
|
||||
@@ -182,11 +182,12 @@ ID Hostname Address Tag Zone Capacit
|
||||
## Creating a cluster layout
|
||||
|
||||
Creating a cluster layout for a Garage deployment means informing Garage
|
||||
of the disk space available on each node of the cluster
|
||||
as well as the zone (e.g. datacenter) each machine is located in.
|
||||
of the disk space available on each node of the cluster, `-c`,
|
||||
as well as the name of the zone (e.g. datacenter), `-z`, each machine is located in.
|
||||
|
||||
For our test deployment, we are using only one node. The way in which we configure
|
||||
it does not matter, you can simply write:
|
||||
For our test deployment, we are have only one node with zone named `dc1` and a
|
||||
capacity of `1G`, though the capacity is ignored for a single node deployment
|
||||
and can be changed later when adding new nodes.
|
||||
|
||||
```bash
|
||||
garage layout assign -z dc1 -c 1G <node_id>
|
||||
|
||||
@@ -46,6 +46,7 @@ bootstrap_peers = [
|
||||
"212fd62eeaca72c122b45a7f4fa0f55e012aa5e24ac384a72a3016413fa724ff@[fc00:F::1]:3901",
|
||||
]
|
||||
|
||||
allow_punycode = false
|
||||
|
||||
[consul_discovery]
|
||||
api = "catalog"
|
||||
@@ -92,29 +93,30 @@ The following gives details about each available configuration option.
|
||||
|
||||
[Environment variables](#env_variables).
|
||||
|
||||
Top-level configuration options:
|
||||
Top-level configuration options, in alphabetical order:
|
||||
[`allow_punycode`](#allow_punycode),
|
||||
[`allow_world_readable_secrets`](#allow_world_readable_secrets),
|
||||
[`block_ram_buffer_max`](#block_ram_buffer_max),
|
||||
[`block_size`](#block_size),
|
||||
[`bootstrap_peers`](#bootstrap_peers),
|
||||
[`compression_level`](#compression_level),
|
||||
[`consistency_mode`](#consistency_mode),
|
||||
[`data_dir`](#data_dir),
|
||||
[`data_fsync`](#data_fsync),
|
||||
[`db_engine`](#db_engine),
|
||||
[`disable_scrub`](#disable_scrub),
|
||||
[`use_local_tz`](#use_local_tz),
|
||||
[`lmdb_map_size`](#lmdb_map_size),
|
||||
[`metadata_auto_snapshot_interval`](#metadata_auto_snapshot_interval),
|
||||
[`metadata_dir`](#metadata_dir),
|
||||
[`metadata_fsync`](#metadata_fsync),
|
||||
[`metadata_snapshots_dir`](#metadata_snapshots_dir),
|
||||
[`replication_factor`](#replication_factor),
|
||||
[`consistency_mode`](#consistency_mode),
|
||||
[`rpc_bind_addr`](#rpc_bind_addr),
|
||||
[`rpc_bind_outgoing`](#rpc_bind_outgoing),
|
||||
[`rpc_public_addr`](#rpc_public_addr),
|
||||
[`rpc_public_addr_subnet`](#rpc_public_addr_subnet)
|
||||
[`rpc_secret`/`rpc_secret_file`](#rpc_secret).
|
||||
[`rpc_secret`/`rpc_secret_file`](#rpc_secret),
|
||||
[`use_local_tz`](#use_local_tz).
|
||||
|
||||
The `[consul_discovery]` section:
|
||||
[`api`](#consul_api),
|
||||
@@ -151,13 +153,17 @@ The `[admin]` section:
|
||||
|
||||
### Environment variables {#env_variables}
|
||||
|
||||
The following configuration parameter must be specified as an environment
|
||||
variable, it does not exist in the configuration file:
|
||||
The following configuration parameters must be specified as environment variables,
|
||||
they do not exist in the configuration file:
|
||||
|
||||
- `GARAGE_LOG_TO_SYSLOG` (since `v0.9.4`): set this to `1` or `true` to make the
|
||||
Garage daemon send its logs to `syslog` (using the libc `syslog` function)
|
||||
instead of printing to stderr.
|
||||
|
||||
- `GARAGE_LOG_TO_JOURNALD` (since `v1.2.0`): set this to `1` or `true` to make the
|
||||
Garage daemon send its logs to `journald` (using the native protocol of `systemd-journald`)
|
||||
instead of printing to stderr.
|
||||
|
||||
The following environment variables can be used to override the corresponding
|
||||
values in the configuration file:
|
||||
|
||||
@@ -169,7 +175,7 @@ values in the configuration file:
|
||||
|
||||
### Top-level configuration options
|
||||
|
||||
#### `replication_factor` {#replication_factor}
|
||||
#### `replication_factor` (since `v1.0.0`) {#replication_factor}
|
||||
|
||||
The replication factor can be any positive integer smaller or equal the node count in your cluster.
|
||||
The chosen replication factor has a big impact on the cluster's failure tolerancy and performance characteristics.
|
||||
@@ -217,7 +223,7 @@ is in progress. In theory, no data should be lost as rebalancing is a
|
||||
routine operation for Garage, although we cannot guarantee you that everything
|
||||
will go right in such an extreme scenario.
|
||||
|
||||
#### `consistency_mode` {#consistency_mode}
|
||||
#### `consistency_mode` (since `v1.0.0`) {#consistency_mode}
|
||||
|
||||
The consistency mode setting determines the read and write behaviour of your cluster.
|
||||
|
||||
@@ -604,7 +610,7 @@ be obtained by running `garage node id` and then included directly in the
|
||||
key will be returned by `garage node id` and you will have to add the IP
|
||||
yourself.
|
||||
|
||||
### `allow_world_readable_secrets` or `GARAGE_ALLOW_WORLD_READABLE_SECRETS` (env) {#allow_world_readable_secrets}
|
||||
#### `allow_world_readable_secrets` or `GARAGE_ALLOW_WORLD_READABLE_SECRETS` (env) {#allow_world_readable_secrets}
|
||||
|
||||
Garage checks the permissions of your secret files to make sure they're not
|
||||
world-readable. In some cases, the check might fail and consider your files as
|
||||
@@ -616,6 +622,13 @@ permission verification.
|
||||
Alternatively, you can set the `GARAGE_ALLOW_WORLD_READABLE_SECRETS`
|
||||
environment variable to `true` to bypass the permissions check.
|
||||
|
||||
#### `allow_punycode` {#allow_punycode}
|
||||
|
||||
Allow creating buckets with names containing punycode. When used for buckets served
|
||||
as websites, this allows using almost any unicode character in the domain name.
|
||||
|
||||
Default to `false`.
|
||||
|
||||
### The `[consul_discovery]` section
|
||||
|
||||
Garage supports discovering other nodes of the cluster using Consul. For this
|
||||
|
||||
@@ -23,7 +23,6 @@ Feel free to open a PR to suggest fixes this table. Minio is missing because the
|
||||
- 2022-05-25 - Many Ceph S3 endpoints are not documented but implemented. Following a notification from the Ceph community, we added them.
|
||||
|
||||
|
||||
|
||||
## High-level features
|
||||
|
||||
| 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) |
|
||||
@@ -34,6 +33,7 @@ Feel free to open a PR to suggest fixes this table. Minio is missing because the
|
||||
| [URL vhost-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access) URL (eg. `bucket.host.tld/key`) | ✅ Implemented | ❌| ✅| ✅ | ✅ |
|
||||
| [Presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) | ✅ Implemented | ❌| ✅ | ✅ | ✅(❓) |
|
||||
| [SSE-C encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html) | ✅ Implemented | ❓ | ✅ | ❌ | ✅ |
|
||||
| [Bucket versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html) | ❌ Missing | ✅ | ✅ | ❌ | ✅ |
|
||||
|
||||
*Note:* OpenIO does not says if it supports presigned URLs. Because it is part
|
||||
of signature v4 and they claim they support it without additional precisions,
|
||||
|
||||
@@ -70,7 +70,7 @@ Example response body:
|
||||
```json
|
||||
{
|
||||
"node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df",
|
||||
"garageVersion": "v1.1.0",
|
||||
"garageVersion": "v1.2.0",
|
||||
"garageFeatures": [
|
||||
"k2v",
|
||||
"lmdb",
|
||||
|
||||
@@ -74,6 +74,7 @@ let
|
||||
"metrics"
|
||||
"telemetry-otlp"
|
||||
"syslog"
|
||||
"journald"
|
||||
]));
|
||||
|
||||
featuresStr = lib.concatStringsSep "," rootFeatures;
|
||||
|
||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: garage
|
||||
description: S3-compatible object store for small self-hosted geo-distributed deployments
|
||||
type: application
|
||||
version: 0.7.0
|
||||
appVersion: "v1.1.0"
|
||||
version: 0.7.1
|
||||
appVersion: "v1.2.0"
|
||||
home: https://garagehq.deuxfleurs.fr/
|
||||
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
|
||||
|
||||
@@ -15,4 +15,4 @@ keywords:
|
||||
sources:
|
||||
- https://git.deuxfleurs.fr/Deuxfleurs/garage.git
|
||||
|
||||
maintainers: []
|
||||
maintainers: []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# garage
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
S3-compatible object store for small self-hosted geo-distributed deployments
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: garagenodes.deuxfleurs.fr
|
||||
spec:
|
||||
conversion:
|
||||
strategy: None
|
||||
group: deuxfleurs.fr
|
||||
names:
|
||||
kind: GarageNode
|
||||
listKind: GarageNodeList
|
||||
plural: garagenodes
|
||||
singular: garagenode
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Auto-generated derived type for Node via `CustomResource`
|
||||
properties:
|
||||
spec:
|
||||
properties:
|
||||
address:
|
||||
format: ip
|
||||
type: string
|
||||
hostname:
|
||||
type: string
|
||||
port:
|
||||
format: uint16
|
||||
minimum: 0
|
||||
type: integer
|
||||
required:
|
||||
- address
|
||||
- hostname
|
||||
- port
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
title: GarageNode
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources: {}
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- garagenodes.deuxfleurs.fr.yaml
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_api_admin"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
@@ -277,7 +277,7 @@ pub async fn handle_create_bucket(
|
||||
let helper = garage.locked_helper().await;
|
||||
|
||||
if let Some(ga) = &req.global_alias {
|
||||
if !is_valid_bucket_name(ga) {
|
||||
if !is_valid_bucket_name(ga, garage.config.allow_punycode) {
|
||||
return Err(Error::bad_request(format!(
|
||||
"{}: {}",
|
||||
ga, INVALID_BUCKET_NAME_MESSAGE
|
||||
@@ -292,7 +292,7 @@ pub async fn handle_create_bucket(
|
||||
}
|
||||
|
||||
if let Some(la) = &req.local_alias {
|
||||
if !is_valid_bucket_name(&la.alias) {
|
||||
if !is_valid_bucket_name(&la.alias, garage.config.allow_punycode) {
|
||||
return Err(Error::bad_request(format!(
|
||||
"{}: {}",
|
||||
la.alias, INVALID_BUCKET_NAME_MESSAGE
|
||||
@@ -382,7 +382,7 @@ pub async fn handle_delete_bucket(
|
||||
for ((key_id, alias), _, active) in state.local_aliases.items().iter() {
|
||||
if *active {
|
||||
helper
|
||||
.unset_local_bucket_alias(bucket.id, key_id, alias)
|
||||
.purge_local_bucket_alias(bucket.id, key_id, alias)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_api_common"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -28,7 +28,6 @@ err-derive.workspace = true
|
||||
hex.workspace = true
|
||||
hmac.workspace = true
|
||||
md-5.workspace = true
|
||||
idna.workspace = true
|
||||
tracing.workspace = true
|
||||
nom.workspace = true
|
||||
pin-project.workspace = true
|
||||
|
||||
@@ -8,7 +8,6 @@ use hyper::{
|
||||
body::{Body, Bytes},
|
||||
Request, Response,
|
||||
};
|
||||
use idna::domain_to_unicode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use garage_model::bucket_table::BucketParams;
|
||||
@@ -97,7 +96,7 @@ pub fn authority_to_host(authority: &str) -> Result<String, Error> {
|
||||
authority
|
||||
))),
|
||||
};
|
||||
authority.map(|h| domain_to_unicode(h).0)
|
||||
authority.map(|h| h.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// Extract the bucket name and the key name from an HTTP path and possibly a bucket provided in
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_api_k2v"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_api_s3"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
@@ -226,6 +226,7 @@ impl ApiHandler for S3ApiServer {
|
||||
Endpoint::DeleteBucket {} => handle_delete_bucket(ctx).await,
|
||||
Endpoint::GetBucketLocation {} => handle_get_bucket_location(ctx),
|
||||
Endpoint::GetBucketVersioning {} => handle_get_bucket_versioning(),
|
||||
Endpoint::GetBucketAcl {} => handle_get_bucket_acl(ctx),
|
||||
Endpoint::ListObjects {
|
||||
delimiter,
|
||||
encoding_type,
|
||||
|
||||
+62
-4
@@ -5,7 +5,7 @@ use hyper::{Request, Response, StatusCode};
|
||||
use garage_model::bucket_alias_table::*;
|
||||
use garage_model::bucket_table::Bucket;
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::key_table::Key;
|
||||
use garage_model::key_table::{Key, KeyParams};
|
||||
use garage_model::permission::BucketKeyPerm;
|
||||
use garage_table::util::*;
|
||||
use garage_util::crdt::*;
|
||||
@@ -44,6 +44,55 @@ pub fn handle_get_bucket_versioning() -> Result<Response<ResBody>, Error> {
|
||||
.body(string_body(xml))?)
|
||||
}
|
||||
|
||||
pub fn handle_get_bucket_acl(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
|
||||
let ReqCtx {
|
||||
bucket_id, api_key, ..
|
||||
} = ctx;
|
||||
let key_p = api_key.params().ok_or_internal_error(
|
||||
"Key should not be in deleted state at this point (in handle_get_bucket_acl)",
|
||||
)?;
|
||||
|
||||
let mut grants: Vec<s3_xml::Grant> = vec![];
|
||||
let kp = api_key.bucket_permissions(&bucket_id);
|
||||
|
||||
if kp.allow_owner {
|
||||
grants.push(s3_xml::Grant {
|
||||
grantee: create_grantee(&key_p, &api_key),
|
||||
permission: s3_xml::Value("FULL_CONTROL".to_string()),
|
||||
});
|
||||
} else {
|
||||
if kp.allow_read {
|
||||
grants.push(s3_xml::Grant {
|
||||
grantee: create_grantee(&key_p, &api_key),
|
||||
permission: s3_xml::Value("READ".to_string()),
|
||||
});
|
||||
grants.push(s3_xml::Grant {
|
||||
grantee: create_grantee(&key_p, &api_key),
|
||||
permission: s3_xml::Value("READ_ACP".to_string()),
|
||||
});
|
||||
}
|
||||
if kp.allow_write {
|
||||
grants.push(s3_xml::Grant {
|
||||
grantee: create_grantee(&key_p, &api_key),
|
||||
permission: s3_xml::Value("WRITE".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let access_control_policy = s3_xml::AccessControlPolicy {
|
||||
xmlns: (),
|
||||
owner: None,
|
||||
acl: s3_xml::AccessControlList { entries: grants },
|
||||
};
|
||||
|
||||
let xml = s3_xml::to_xml_with_header(&access_control_policy)?;
|
||||
trace!("xml: {}", xml);
|
||||
|
||||
Ok(Response::builder()
|
||||
.header("Content-Type", "application/xml")
|
||||
.body(string_body(xml))?)
|
||||
}
|
||||
|
||||
pub async fn handle_list_buckets(
|
||||
garage: &Garage,
|
||||
api_key: &Key,
|
||||
@@ -172,7 +221,7 @@ pub async fn handle_create_bucket(
|
||||
}
|
||||
|
||||
// Create the bucket!
|
||||
if !is_valid_bucket_name(&bucket_name) {
|
||||
if !is_valid_bucket_name(&bucket_name, garage.config.allow_punycode) {
|
||||
return Err(Error::bad_request(format!(
|
||||
"{}: {}",
|
||||
bucket_name, INVALID_BUCKET_NAME_MESSAGE
|
||||
@@ -241,11 +290,11 @@ pub async fn handle_delete_bucket(ctx: ReqCtx) -> Result<Response<ResBody>, Erro
|
||||
// 1. delete bucket alias
|
||||
if is_local_alias {
|
||||
helper
|
||||
.unset_local_bucket_alias(*bucket_id, &api_key.key_id, bucket_name)
|
||||
.purge_local_bucket_alias(*bucket_id, &api_key.key_id, bucket_name)
|
||||
.await?;
|
||||
} else {
|
||||
helper
|
||||
.unset_global_bucket_alias(*bucket_id, bucket_name)
|
||||
.purge_global_bucket_alias(*bucket_id, bucket_name)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -311,6 +360,15 @@ fn parse_create_bucket_xml(xml_bytes: &[u8]) -> Option<Option<String>> {
|
||||
Some(ret)
|
||||
}
|
||||
|
||||
fn create_grantee(key_params: &KeyParams, api_key: &Key) -> s3_xml::Grantee {
|
||||
s3_xml::Grantee {
|
||||
xmlns_xsi: (),
|
||||
typ: "CanonicalUser".to_string(),
|
||||
display_name: Some(s3_xml::Value(key_params.name.get().to_string())),
|
||||
id: Some(s3_xml::Value(api_key.key_id.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+62
-29
@@ -26,9 +26,10 @@ use garage_api_common::signature::checksum::*;
|
||||
use crate::api_server::{ReqBody, ResBody};
|
||||
use crate::encryption::EncryptionParams;
|
||||
use crate::error::*;
|
||||
use crate::get::{full_object_byte_stream, PreconditionHeaders};
|
||||
use crate::get::{check_version_not_deleted, full_object_byte_stream, PreconditionHeaders};
|
||||
use crate::multipart;
|
||||
use crate::put::{extract_metadata_headers, save_stream, ChecksumMode, SaveStreamResult};
|
||||
use crate::website::X_AMZ_WEBSITE_REDIRECT_LOCATION;
|
||||
use crate::xml::{self as s3_xml, xmlns_tag};
|
||||
|
||||
pub const X_AMZ_COPY_SOURCE_IF_MATCH: HeaderName =
|
||||
@@ -84,7 +85,18 @@ pub async fn handle_copy(
|
||||
Some(v) if v == hyper::header::HeaderValue::from_static("REPLACE") => {
|
||||
extract_metadata_headers(req.headers())?
|
||||
}
|
||||
_ => source_object_meta_inner.into_owned().headers,
|
||||
_ => {
|
||||
// The x-amz-website-redirect-location header is not copied, instead
|
||||
// it is replaced by the value from the request (or removed if no
|
||||
// value was specified)
|
||||
let is_redirect =
|
||||
|(key, _): &(String, String)| key == X_AMZ_WEBSITE_REDIRECT_LOCATION.as_str();
|
||||
let mut headers: Vec<_> = source_object_meta_inner.headers.clone();
|
||||
headers.retain(|h| !is_redirect(h));
|
||||
let new_headers = extract_metadata_headers(req.headers())?;
|
||||
headers.extend(new_headers.into_iter().filter(is_redirect));
|
||||
headers
|
||||
}
|
||||
},
|
||||
checksum: source_checksum,
|
||||
};
|
||||
@@ -225,6 +237,7 @@ async fn handle_copy_metaonly(
|
||||
.get(&source_version.uuid, &EmptyKey)
|
||||
.await?;
|
||||
let source_version = source_version.ok_or(Error::NoSuchKey)?;
|
||||
check_version_not_deleted(&source_version)?;
|
||||
|
||||
// Write an "uploading" marker in Object table
|
||||
// This holds a reference to the object in the Version table
|
||||
@@ -416,6 +429,7 @@ pub async fn handle_upload_part_copy(
|
||||
.get(&source_object_version.uuid, &EmptyKey)
|
||||
.await?
|
||||
.ok_or(Error::NoSuchKey)?;
|
||||
check_version_not_deleted(&source_version)?;
|
||||
|
||||
// We want to reuse blocks from the source version as much as possible.
|
||||
// However, we still need to get the data from these blocks
|
||||
@@ -547,6 +561,7 @@ pub async fn handle_upload_part_copy(
|
||||
|
||||
let mut current_offset = 0;
|
||||
let mut next_block = defragmenter.next().await?;
|
||||
let mut blocks_to_dup = dest_version.clone();
|
||||
|
||||
// TODO this could be optimized similarly to read_and_put_blocks
|
||||
// low priority because uploadpartcopy is rarely used
|
||||
@@ -576,8 +591,7 @@ pub async fn handle_upload_part_copy(
|
||||
.unwrap()?;
|
||||
checksummer = checksummer_updated;
|
||||
|
||||
dest_version.blocks.clear();
|
||||
dest_version.blocks.put(
|
||||
let (version_block_key, version_block) = (
|
||||
VersionBlockKey {
|
||||
part_number,
|
||||
offset: current_offset,
|
||||
@@ -589,37 +603,56 @@ pub async fn handle_upload_part_copy(
|
||||
);
|
||||
current_offset += data_len;
|
||||
|
||||
let block_ref = BlockRef {
|
||||
block: final_hash,
|
||||
version: dest_version_id,
|
||||
deleted: false.into(),
|
||||
let next = if let Some(final_data) = data_to_upload {
|
||||
dest_version.blocks.clear();
|
||||
dest_version.blocks.put(version_block_key, version_block);
|
||||
let block_ref = BlockRef {
|
||||
block: final_hash,
|
||||
version: dest_version_id,
|
||||
deleted: false.into(),
|
||||
};
|
||||
let (_, _, _, next) = futures::try_join!(
|
||||
// Thing 1: if the block is not exactly a block that existed before,
|
||||
// we need to insert that data as a new block.
|
||||
garage.block_manager.rpc_put_block(
|
||||
final_hash,
|
||||
final_data,
|
||||
dest_encryption.is_encrypted(),
|
||||
None
|
||||
),
|
||||
// Thing 2: we need to insert the block in the version
|
||||
garage.version_table.insert(&dest_version),
|
||||
// Thing 3: we need to add a block reference
|
||||
garage.block_ref_table.insert(&block_ref),
|
||||
// Thing 4: we need to read the next block
|
||||
defragmenter.next(),
|
||||
)?;
|
||||
next
|
||||
} else {
|
||||
blocks_to_dup.blocks.put(version_block_key, version_block);
|
||||
defragmenter.next().await?
|
||||
};
|
||||
|
||||
let (_, _, _, next) = futures::try_join!(
|
||||
// Thing 1: if the block is not exactly a block that existed before,
|
||||
// we need to insert that data as a new block.
|
||||
async {
|
||||
if let Some(final_data) = data_to_upload {
|
||||
garage
|
||||
.block_manager
|
||||
.rpc_put_block(final_hash, final_data, dest_encryption.is_encrypted(), None)
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
// Thing 2: we need to insert the block in the version
|
||||
garage.version_table.insert(&dest_version),
|
||||
// Thing 3: we need to add a block reference
|
||||
garage.block_ref_table.insert(&block_ref),
|
||||
// Thing 4: we need to read the next block
|
||||
defragmenter.next(),
|
||||
)?;
|
||||
next_block = next;
|
||||
}
|
||||
|
||||
assert_eq!(current_offset, source_range.length);
|
||||
|
||||
// Put the duplicated blocks into the version & block_refs tables
|
||||
let block_refs_to_put = blocks_to_dup
|
||||
.blocks
|
||||
.items()
|
||||
.iter()
|
||||
.map(|b| BlockRef {
|
||||
block: b.1.hash,
|
||||
version: dest_version_id,
|
||||
deleted: false.into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
futures::try_join!(
|
||||
garage.version_table.insert(&blocks_to_dup),
|
||||
garage.block_ref_table.insert_many(&block_refs_to_put[..]),
|
||||
)?;
|
||||
|
||||
let checksums = checksummer.finalize();
|
||||
let etag = dest_encryption.etag_from_md5(&checksums.md5);
|
||||
let checksum = checksums.extract(dest_object_checksum_algorithm);
|
||||
|
||||
+30
-2
@@ -19,12 +19,13 @@ use garage_net::stream::ByteStream;
|
||||
use garage_rpc::rpc_helper::OrderTag;
|
||||
use garage_table::EmptyKey;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::OkOrMessage;
|
||||
use garage_util::error::{Error as UtilError, OkOrMessage};
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::s3::object_table::*;
|
||||
use garage_model::s3::version_table::*;
|
||||
|
||||
use garage_api_common::common_error::CommonError;
|
||||
use garage_api_common::helpers::*;
|
||||
use garage_api_common::signature::checksum::{add_checksum_response_headers, X_AMZ_CHECKSUM_MODE};
|
||||
|
||||
@@ -215,6 +216,7 @@ pub async fn handle_head_without_ctx(
|
||||
.get(&object_version.uuid, &EmptyKey)
|
||||
.await?
|
||||
.ok_or(Error::NoSuchKey)?;
|
||||
check_version_not_deleted(&version)?;
|
||||
|
||||
let (part_offset, part_end) =
|
||||
calculate_part_bounds(&version, pn).ok_or(Error::InvalidPart)?;
|
||||
@@ -365,6 +367,21 @@ pub async fn handle_get_without_ctx(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_version_not_deleted(version: &Version) -> Result<(), Error> {
|
||||
if version.deleted.get() {
|
||||
// the version was deleted between when the object_table was consulted
|
||||
// and now, this could mean the object was deleted, or overriden.
|
||||
// Rather than say the key doesn't exist, return a transient error
|
||||
// to signal the client to try again.
|
||||
return Err(CommonError::InternalError(UtilError::Message(
|
||||
"conflict/inconsistency between object and version state, version is deleted"
|
||||
.to_string(),
|
||||
))
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_get_full(
|
||||
garage: Arc<Garage>,
|
||||
version: &ObjectVersion,
|
||||
@@ -431,6 +448,7 @@ pub fn full_object_byte_stream(
|
||||
.ok_or_message("channel closed")?;
|
||||
|
||||
let version = version_fut.await.unwrap()?.ok_or(Error::NoSuchKey)?;
|
||||
check_version_not_deleted(&version)?;
|
||||
for (i, (_, vb)) in version.blocks.items().iter().enumerate().skip(1) {
|
||||
let stream_block_i = encryption
|
||||
.get_block(&garage, &vb.hash, Some(order_stream.order(i as u64)))
|
||||
@@ -446,6 +464,14 @@ pub fn full_object_byte_stream(
|
||||
{
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
// TODO i think this is a bad idea, we should log
|
||||
// an error and stop there. If the error happens to
|
||||
// be exactly the size of what hasn't been streamed
|
||||
// yet, the client will see the request as a
|
||||
// success
|
||||
// instead truncating the output notify the client
|
||||
// something happened with their download, so that
|
||||
// they can retry it
|
||||
let _ = tx.send(error_stream_item(e)).await;
|
||||
}
|
||||
}
|
||||
@@ -497,7 +523,7 @@ async fn handle_get_range(
|
||||
.get(&version.uuid, &EmptyKey)
|
||||
.await?
|
||||
.ok_or(Error::NoSuchKey)?;
|
||||
|
||||
check_version_not_deleted(&version)?;
|
||||
let body =
|
||||
body_from_blocks_range(garage, encryption, version.blocks.items(), begin, end);
|
||||
Ok(resp_builder.body(body)?)
|
||||
@@ -548,6 +574,8 @@ async fn handle_get_part(
|
||||
.await?
|
||||
.ok_or(Error::NoSuchKey)?;
|
||||
|
||||
check_version_not_deleted(&version)?;
|
||||
|
||||
let (begin, end) =
|
||||
calculate_part_bounds(&version, part_number).ok_or(Error::InvalidPart)?;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ pub async fn handle_get_lifecycle(ctx: ReqCtx) -> Result<Response<ResBody>, Erro
|
||||
.body(string_body(xml))?)
|
||||
} else {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(empty_body())?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ pub fn xmlns_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_str("http://s3.amazonaws.com/doc/2006-03-01/")
|
||||
}
|
||||
|
||||
pub fn xmlns_xsi_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_str("http://www.w3.org/2001/XMLSchema-instance")
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Value(#[serde(rename = "$value")] pub String);
|
||||
|
||||
@@ -319,6 +323,42 @@ pub struct PostObject {
|
||||
pub etag: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct Grantee {
|
||||
#[serde(rename = "xmlns:xsi", serialize_with = "xmlns_xsi_tag")]
|
||||
pub xmlns_xsi: (),
|
||||
#[serde(rename = "xsi:type")]
|
||||
pub typ: String,
|
||||
#[serde(rename = "DisplayName")]
|
||||
pub display_name: Option<Value>,
|
||||
#[serde(rename = "ID")]
|
||||
pub id: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct Grant {
|
||||
#[serde(rename = "Grantee")]
|
||||
pub grantee: Grantee,
|
||||
#[serde(rename = "Permission")]
|
||||
pub permission: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct AccessControlList {
|
||||
#[serde(rename = "Grant")]
|
||||
pub entries: Vec<Grant>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct AccessControlPolicy {
|
||||
#[serde(serialize_with = "xmlns_tag")]
|
||||
pub xmlns: (),
|
||||
#[serde(rename = "Owner")]
|
||||
pub owner: Option<Owner>,
|
||||
#[serde(rename = "AccessControlList")]
|
||||
pub acl: AccessControlList,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -427,6 +467,43 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_bucket_acl_result() -> Result<(), ApiError> {
|
||||
let grant = Grant {
|
||||
grantee: Grantee {
|
||||
xmlns_xsi: (),
|
||||
typ: "CanonicalUser".to_string(),
|
||||
display_name: Some(Value("owner_name".to_string())),
|
||||
id: Some(Value("qsdfjklm".to_string())),
|
||||
},
|
||||
permission: Value("FULL_CONTROL".to_string()),
|
||||
};
|
||||
|
||||
let get_bucket_acl = AccessControlPolicy {
|
||||
xmlns: (),
|
||||
owner: None,
|
||||
acl: AccessControlList {
|
||||
entries: vec![grant],
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
to_xml_with_header(&get_bucket_acl)?,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
|
||||
<AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
|
||||
<AccessControlList>\
|
||||
<Grant>\
|
||||
<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\">\
|
||||
<DisplayName>owner_name</DisplayName>\
|
||||
<ID>qsdfjklm</ID>\
|
||||
</Grantee>\
|
||||
<Permission>FULL_CONTROL</Permission>\
|
||||
</Grant>\
|
||||
</AccessControlList>\
|
||||
</AccessControlPolicy>"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_result() -> Result<(), ApiError> {
|
||||
let delete_result = DeleteResult {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_block"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
@@ -17,7 +17,6 @@ use opentelemetry::{
|
||||
Context,
|
||||
};
|
||||
|
||||
use garage_net::endpoint::RpcInFlightLimiter;
|
||||
use garage_net::stream::{read_stream_to_end, stream_asyncread, ByteStream};
|
||||
|
||||
use garage_db as db;
|
||||
@@ -296,7 +295,6 @@ impl BlockManager {
|
||||
&node_id,
|
||||
BlockRpc::GetBlock(*hash, order_tag),
|
||||
priority,
|
||||
RpcInFlightLimiter::TableWrite,
|
||||
);
|
||||
tokio::select! {
|
||||
res = rpc => {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_db"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -57,6 +57,7 @@ opentelemetry.workspace = true
|
||||
opentelemetry-prometheus = { workspace = true, optional = true }
|
||||
opentelemetry-otlp = { workspace = true, optional = true }
|
||||
syslog-tracing = { workspace = true, optional = true }
|
||||
tracing-journald = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
garage_api_common.workspace = true
|
||||
@@ -101,6 +102,8 @@ metrics = [ "garage_api_admin/metrics", "opentelemetry-prometheus" ]
|
||||
telemetry-otlp = [ "opentelemetry-otlp" ]
|
||||
# Logging to syslog
|
||||
syslog = [ "syslog-tracing" ]
|
||||
# Logging to journald
|
||||
journald = [ "tracing-journald" ]
|
||||
|
||||
# NOTE: bundled-libs and system-libs should be treat as mutually exclusive;
|
||||
# exactly one of them should be enabled.
|
||||
|
||||
@@ -101,6 +101,7 @@ impl AdminRpcHandler {
|
||||
let mut obj_dels = 0;
|
||||
let mut mpu_dels = 0;
|
||||
let mut ver_dels = 0;
|
||||
let mut br_dels = 0;
|
||||
|
||||
for hash in blocks {
|
||||
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
|
||||
@@ -131,12 +132,19 @@ impl AdminRpcHandler {
|
||||
ver_dels += 1;
|
||||
}
|
||||
}
|
||||
if !br.deleted.get() {
|
||||
let mut br = br;
|
||||
br.deleted.set();
|
||||
self.garage.block_ref_table.insert(&br).await?;
|
||||
br_dels += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AdminRpc::Ok(format!(
|
||||
"Purged {} blocks, {} versions, {} objects, {} multipart uploads",
|
||||
"Purged {} blocks: marked {} block refs, {} versions, {} objects and {} multipart uploads as deleted",
|
||||
blocks.len(),
|
||||
br_dels,
|
||||
ver_dels,
|
||||
obj_dels,
|
||||
mpu_dels,
|
||||
|
||||
@@ -126,7 +126,7 @@ impl AdminRpcHandler {
|
||||
|
||||
#[allow(clippy::ptr_arg)]
|
||||
async fn handle_create_bucket(&self, name: &String) -> Result<AdminRpc, Error> {
|
||||
if !is_valid_bucket_name(name) {
|
||||
if !is_valid_bucket_name(name, self.garage.config.allow_punycode) {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"{}: {}",
|
||||
name, INVALID_BUCKET_NAME_MESSAGE
|
||||
|
||||
+1
-12
@@ -13,8 +13,6 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use format_table::format_table_to_string;
|
||||
|
||||
use garage_net::endpoint::RpcInFlightLimiter;
|
||||
|
||||
use garage_util::background::BackgroundRunner;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::Error as GarageError;
|
||||
@@ -120,7 +118,6 @@ impl AdminRpcHandler {
|
||||
&node,
|
||||
AdminRpc::LaunchRepair(opt_to_send.clone()),
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.await;
|
||||
if !matches!(resp, Ok(Ok(_))) {
|
||||
@@ -167,12 +164,7 @@ impl AdminRpcHandler {
|
||||
let node_id = (*node).into();
|
||||
match self
|
||||
.endpoint
|
||||
.call(
|
||||
&node_id,
|
||||
AdminRpc::Stats(opt),
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.call(&node_id, AdminRpc::Stats(opt), PRIO_NORMAL)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(AdminRpc::Ok(s))) => writeln!(&mut ret, "{}", s).unwrap(),
|
||||
@@ -415,7 +407,6 @@ impl AdminRpcHandler {
|
||||
variable: variable.clone(),
|
||||
}),
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.await??
|
||||
{
|
||||
@@ -465,7 +456,6 @@ impl AdminRpcHandler {
|
||||
value: value.to_string(),
|
||||
}),
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.await??
|
||||
{
|
||||
@@ -498,7 +488,6 @@ impl AdminRpcHandler {
|
||||
&to,
|
||||
AdminRpc::MetaOperation(MetaOperation::Snapshot { all: false }),
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.await?
|
||||
}))
|
||||
|
||||
+3
-17
@@ -2,7 +2,6 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::time::Duration;
|
||||
|
||||
use format_table::format_table;
|
||||
use garage_net::endpoint::RpcInFlightLimiter;
|
||||
use garage_util::error::*;
|
||||
|
||||
use garage_rpc::layout::*;
|
||||
@@ -201,12 +200,7 @@ pub async fn cmd_connect(
|
||||
args: ConnectNodeOpt,
|
||||
) -> Result<(), Error> {
|
||||
match rpc_cli
|
||||
.call(
|
||||
&rpc_host,
|
||||
SystemRpc::Connect(args.node),
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.call(&rpc_host, SystemRpc::Connect(args.node), PRIO_NORMAL)
|
||||
.await??
|
||||
{
|
||||
SystemRpc::Ok => {
|
||||
@@ -222,10 +216,7 @@ pub async fn cmd_admin(
|
||||
rpc_host: NodeID,
|
||||
args: AdminRpc,
|
||||
) -> Result<(), HelperError> {
|
||||
match rpc_cli
|
||||
.call(&rpc_host, args, PRIO_NORMAL, RpcInFlightLimiter::NoLimit)
|
||||
.await??
|
||||
{
|
||||
match rpc_cli.call(&rpc_host, args, PRIO_NORMAL).await?? {
|
||||
AdminRpc::Ok(msg) => {
|
||||
println!("{}", msg);
|
||||
}
|
||||
@@ -280,12 +271,7 @@ pub async fn fetch_status(
|
||||
rpc_host: NodeID,
|
||||
) -> Result<Vec<KnownNodeInfo>, Error> {
|
||||
match rpc_cli
|
||||
.call(
|
||||
&rpc_host,
|
||||
SystemRpc::GetKnownNodes,
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.call(&rpc_host, SystemRpc::GetKnownNodes, PRIO_NORMAL)
|
||||
.await??
|
||||
{
|
||||
SystemRpc::ReturnKnownNodes(nodes) => Ok(nodes),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use bytesize::ByteSize;
|
||||
|
||||
use format_table::format_table;
|
||||
use garage_net::endpoint::RpcInFlightLimiter;
|
||||
use garage_util::crdt::Crdt;
|
||||
use garage_util::error::*;
|
||||
|
||||
@@ -46,12 +45,7 @@ pub async fn cmd_assign_role(
|
||||
args: AssignRoleOpt,
|
||||
) -> Result<(), Error> {
|
||||
let status = match rpc_cli
|
||||
.call(
|
||||
&rpc_host,
|
||||
SystemRpc::GetKnownNodes,
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.call(&rpc_host, SystemRpc::GetKnownNodes, PRIO_NORMAL)
|
||||
.await??
|
||||
{
|
||||
SystemRpc::ReturnKnownNodes(nodes) => nodes,
|
||||
@@ -481,12 +475,7 @@ pub async fn fetch_layout(
|
||||
rpc_host: NodeID,
|
||||
) -> Result<LayoutHistory, Error> {
|
||||
match rpc_cli
|
||||
.call(
|
||||
&rpc_host,
|
||||
SystemRpc::PullClusterLayout,
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.call(&rpc_host, SystemRpc::PullClusterLayout, PRIO_NORMAL)
|
||||
.await??
|
||||
{
|
||||
SystemRpc::AdvertiseClusterLayout(t) => Ok(t),
|
||||
@@ -504,7 +493,6 @@ pub async fn send_layout(
|
||||
&rpc_host,
|
||||
SystemRpc::AdvertiseClusterLayout(layout),
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.await??;
|
||||
Ok(())
|
||||
|
||||
@@ -478,6 +478,9 @@ pub enum RepairWhat {
|
||||
/// Recalculate block reference counters
|
||||
#[structopt(name = "block-rc", version = garage_version())]
|
||||
BlockRc,
|
||||
/// Fix inconsistency in bucket aliases (WARNING: EXPERIMENTAL)
|
||||
#[structopt(name = "aliases", version = garage_version())]
|
||||
Aliases,
|
||||
/// Verify integrity of all blocks on disc
|
||||
#[structopt(name = "scrub", version = garage_version())]
|
||||
Scrub {
|
||||
|
||||
+38
-1
@@ -208,6 +208,43 @@ fn init_logging(opt: &Opt) {
|
||||
}
|
||||
}
|
||||
|
||||
if std::env::var("GARAGE_LOG_TO_JOURNALD")
|
||||
.map(|x| x == "1" || x == "true")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
#[cfg(feature = "journald")]
|
||||
{
|
||||
use tracing_journald::{Priority, PriorityMappings};
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
let registry = tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::fmt::layer().with_writer(std::io::sink))
|
||||
.with(env_filter);
|
||||
match tracing_journald::layer() {
|
||||
Ok(layer) => {
|
||||
registry
|
||||
.with(layer.with_priority_mappings(PriorityMappings {
|
||||
info: Priority::Informational,
|
||||
debug: Priority::Debug,
|
||||
..PriorityMappings::new()
|
||||
}))
|
||||
.init();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Couldn't connect to journald: {}.", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#[cfg(not(feature = "journald"))]
|
||||
{
|
||||
eprintln!("Journald support is not enabled in this build.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_env_filter(env_filter)
|
||||
@@ -244,7 +281,7 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
|
||||
// Generate a temporary keypair for our RPC client
|
||||
let (_pk, sk) = sodiumoxide::crypto::sign::ed25519::gen_keypair();
|
||||
|
||||
let netapp = NetApp::new(GARAGE_VERSION_TAG, network_key, sk, None, None);
|
||||
let netapp = NetApp::new(GARAGE_VERSION_TAG, network_key, sk, None);
|
||||
|
||||
// Find and parse the address of the target host
|
||||
let (id, addr, is_default_addr) = if let Some(h) = opt.rpc_host {
|
||||
|
||||
@@ -88,6 +88,10 @@ pub async fn launch_online_repair(
|
||||
garage.block_manager.clone(),
|
||||
));
|
||||
}
|
||||
RepairWhat::Aliases => {
|
||||
info!("Repairing bucket aliases (foreground)");
|
||||
garage.locked_helper().await.repair_aliases().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ rpc_bind_addr = "127.0.0.1:{rpc_port}"
|
||||
rpc_public_addr = "127.0.0.1:{rpc_port}"
|
||||
rpc_secret = "{secret}"
|
||||
|
||||
allow_punycode = true
|
||||
|
||||
[s3_api]
|
||||
s3_region = "{region}"
|
||||
api_bind_addr = "127.0.0.1:{s3_port}"
|
||||
|
||||
@@ -533,3 +533,118 @@ async fn test_website_check_domain() {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_website_puny() {
|
||||
const BCKT_NAME: &str = "xn--pda.eu";
|
||||
let ctx = common::context();
|
||||
let bucket = ctx.create_bucket(BCKT_NAME);
|
||||
|
||||
let data = ByteStream::from_static(BODY);
|
||||
|
||||
ctx.client
|
||||
.put_object()
|
||||
.bucket(&bucket)
|
||||
.key("index.html")
|
||||
.body(data)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||
|
||||
let req = |suffix| {
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("http://127.0.0.1:{}/", ctx.garage.web_port))
|
||||
.header("Host", format!("{}{}", BCKT_NAME, suffix))
|
||||
.body(Body::new(Bytes::new()))
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
ctx.garage
|
||||
.command()
|
||||
.args(["bucket", "website", "--allow", BCKT_NAME])
|
||||
.quiet()
|
||||
.expect_success_status("Could not allow website on bucket");
|
||||
|
||||
let mut resp = client.request(req("")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.into_body().collect().await.unwrap().to_bytes(),
|
||||
BODY.as_ref()
|
||||
);
|
||||
|
||||
resp = client.request(req(".web.garage")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.into_body().collect().await.unwrap().to_bytes(),
|
||||
BODY.as_ref()
|
||||
);
|
||||
|
||||
for bname in [
|
||||
BCKT_NAME.to_string(),
|
||||
format!("{BCKT_NAME}.web.garage"),
|
||||
format!("{BCKT_NAME}.s3.garage"),
|
||||
] {
|
||||
let admin_req = || {
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!(
|
||||
"http://127.0.0.1:{0}/check?domain={1}",
|
||||
ctx.garage.admin_port, bname
|
||||
))
|
||||
.body(Body::new(Bytes::new()))
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let admin_resp = client.request(admin_req()).await.unwrap();
|
||||
assert_eq!(admin_resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
admin_resp.into_body().collect().await.unwrap().to_bytes(),
|
||||
format!("Domain '{bname}' is managed by Garage").as_bytes()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_website_object_not_found() {
|
||||
const BCKT_NAME: &str = "not-found";
|
||||
let ctx = common::context();
|
||||
let _bucket = ctx.create_bucket(BCKT_NAME);
|
||||
|
||||
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||
|
||||
let req = |suffix| {
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("http://127.0.0.1:{}/", ctx.garage.web_port))
|
||||
.header("Host", format!("{}{}", BCKT_NAME, suffix))
|
||||
.body(Body::new(Bytes::new()))
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
ctx.garage
|
||||
.command()
|
||||
.args(["bucket", "website", "--allow", BCKT_NAME])
|
||||
.quiet()
|
||||
.expect_success_status("Could not allow website on bucket");
|
||||
|
||||
let resp = client.request(req("")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
// the error we return by default are *not* xml
|
||||
assert_eq!(
|
||||
resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
|
||||
"text/html; charset=utf-8"
|
||||
);
|
||||
let result = String::from_utf8(
|
||||
resp.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(result.contains("not found"));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_model"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
@@ -22,14 +22,10 @@ mod v08 {
|
||||
pub use v08::*;
|
||||
|
||||
impl BucketAlias {
|
||||
pub fn new(name: String, ts: u64, bucket_id: Option<Uuid>) -> Option<Self> {
|
||||
if !is_valid_bucket_name(&name) {
|
||||
None
|
||||
} else {
|
||||
Some(BucketAlias {
|
||||
name,
|
||||
state: crdt::Lww::raw(ts, bucket_id),
|
||||
})
|
||||
pub fn new(name: String, ts: u64, bucket_id: Option<Uuid>) -> Self {
|
||||
BucketAlias {
|
||||
name,
|
||||
state: crdt::Lww::raw(ts, bucket_id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +76,7 @@ impl TableSchema for BucketAliasTable {
|
||||
/// In the case of Garage, bucket names must not be hex-encoded
|
||||
/// 32 byte string, which is excluded thanks to the
|
||||
/// maximum length of 63 bytes given in the spec.
|
||||
pub fn is_valid_bucket_name(n: &str) -> bool {
|
||||
pub fn is_valid_bucket_name(n: &str, puny: bool) -> bool {
|
||||
// Bucket names must be between 3 and 63 characters
|
||||
n.len() >= 3 && n.len() <= 63
|
||||
// Bucket names must be composed of lowercase letters, numbers,
|
||||
@@ -92,7 +88,9 @@ pub fn is_valid_bucket_name(n: &str) -> bool {
|
||||
// Bucket names must not be formatted as an IP address
|
||||
&& n.parse::<std::net::IpAddr>().is_err()
|
||||
// Bucket names must not start with "xn--"
|
||||
&& !n.starts_with("xn--")
|
||||
&& (!n.starts_with("xn--") || puny)
|
||||
// We are a bit stricter, to properly restrict punycode in all labels
|
||||
&& (!n.contains(".xn--") || puny)
|
||||
// Bucket names must not end with "-s3alias"
|
||||
&& !n.ends_with("-s3alias")
|
||||
}
|
||||
|
||||
+7
-32
@@ -175,13 +175,7 @@ impl Garage {
|
||||
|
||||
// ---- admin tables ----
|
||||
info!("Initialize bucket_table...");
|
||||
let bucket_table = Table::new(
|
||||
BucketTable,
|
||||
control_rep_param.clone(),
|
||||
system.clone(),
|
||||
&db,
|
||||
&config.experimental.merkle_backpressure,
|
||||
);
|
||||
let bucket_table = Table::new(BucketTable, control_rep_param.clone(), system.clone(), &db);
|
||||
|
||||
info!("Initialize bucket_alias_table...");
|
||||
let bucket_alias_table = Table::new(
|
||||
@@ -189,16 +183,9 @@ impl Garage {
|
||||
control_rep_param.clone(),
|
||||
system.clone(),
|
||||
&db,
|
||||
&config.experimental.merkle_backpressure,
|
||||
);
|
||||
info!("Initialize key_table_table...");
|
||||
let key_table = Table::new(
|
||||
KeyTable,
|
||||
control_rep_param,
|
||||
system.clone(),
|
||||
&db,
|
||||
&config.experimental.merkle_backpressure,
|
||||
);
|
||||
let key_table = Table::new(KeyTable, control_rep_param, system.clone(), &db);
|
||||
|
||||
// ---- S3 tables ----
|
||||
info!("Initialize block_ref_table...");
|
||||
@@ -209,7 +196,6 @@ impl Garage {
|
||||
meta_rep_param.clone(),
|
||||
system.clone(),
|
||||
&db,
|
||||
&config.experimental.merkle_backpressure,
|
||||
);
|
||||
|
||||
info!("Initialize version_table...");
|
||||
@@ -220,12 +206,10 @@ impl Garage {
|
||||
meta_rep_param.clone(),
|
||||
system.clone(),
|
||||
&db,
|
||||
&config.experimental.merkle_backpressure,
|
||||
);
|
||||
|
||||
info!("Initialize multipart upload counter table...");
|
||||
let mpu_counter_table =
|
||||
IndexCounter::new(system.clone(), meta_rep_param.clone(), &db, &config);
|
||||
let mpu_counter_table = IndexCounter::new(system.clone(), meta_rep_param.clone(), &db);
|
||||
|
||||
info!("Initialize multipart upload table...");
|
||||
let mpu_table = Table::new(
|
||||
@@ -236,12 +220,10 @@ impl Garage {
|
||||
meta_rep_param.clone(),
|
||||
system.clone(),
|
||||
&db,
|
||||
&config.experimental.merkle_backpressure,
|
||||
);
|
||||
|
||||
info!("Initialize object counter table...");
|
||||
let object_counter_table =
|
||||
IndexCounter::new(system.clone(), meta_rep_param.clone(), &db, &config);
|
||||
let object_counter_table = IndexCounter::new(system.clone(), meta_rep_param.clone(), &db);
|
||||
|
||||
info!("Initialize object_table...");
|
||||
#[allow(clippy::redundant_clone)]
|
||||
@@ -254,7 +236,6 @@ impl Garage {
|
||||
meta_rep_param.clone(),
|
||||
system.clone(),
|
||||
&db,
|
||||
&config.experimental.merkle_backpressure,
|
||||
);
|
||||
|
||||
info!("Load lifecycle worker state...");
|
||||
@@ -264,7 +245,7 @@ impl Garage {
|
||||
|
||||
// ---- K2V ----
|
||||
#[cfg(feature = "k2v")]
|
||||
let k2v = GarageK2V::new(system.clone(), &db, meta_rep_param, &config);
|
||||
let k2v = GarageK2V::new(system.clone(), &db, meta_rep_param);
|
||||
|
||||
// ---- setup block refcount recalculation ----
|
||||
// this function can be used to fix inconsistencies in the RC table
|
||||
@@ -354,14 +335,9 @@ impl Garage {
|
||||
|
||||
#[cfg(feature = "k2v")]
|
||||
impl GarageK2V {
|
||||
fn new(
|
||||
system: Arc<System>,
|
||||
db: &db::Db,
|
||||
meta_rep_param: TableShardedReplication,
|
||||
config: &Config,
|
||||
) -> Self {
|
||||
fn new(system: Arc<System>, db: &db::Db, meta_rep_param: TableShardedReplication) -> Self {
|
||||
info!("Initialize K2V counter table...");
|
||||
let counter_table = IndexCounter::new(system.clone(), meta_rep_param.clone(), db, config);
|
||||
let counter_table = IndexCounter::new(system.clone(), meta_rep_param.clone(), db);
|
||||
|
||||
info!("Initialize K2V subscription manager...");
|
||||
let subscriptions = Arc::new(SubscriptionManager::new());
|
||||
@@ -375,7 +351,6 @@ impl GarageK2V {
|
||||
meta_rep_param,
|
||||
system.clone(),
|
||||
db,
|
||||
&config.experimental.merkle_backpressure,
|
||||
);
|
||||
|
||||
info!("Initialize K2V RPC handler...");
|
||||
|
||||
+277
-41
@@ -1,3 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use garage_db as db;
|
||||
|
||||
use garage_util::crdt::*;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::{Error as GarageError, OkOrMessage};
|
||||
@@ -47,6 +51,10 @@ impl<'a> LockedHelper<'a> {
|
||||
KeyHelper(self.0)
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// global bucket aliases
|
||||
// ================================================
|
||||
|
||||
/// Sets a new alias for a bucket in global namespace.
|
||||
/// This function fails if:
|
||||
/// - alias name is not valid according to S3 spec
|
||||
@@ -57,7 +65,7 @@ impl<'a> LockedHelper<'a> {
|
||||
bucket_id: Uuid,
|
||||
alias_name: &String,
|
||||
) -> Result<(), Error> {
|
||||
if !is_valid_bucket_name(alias_name) {
|
||||
if !is_valid_bucket_name(alias_name, self.0.config.allow_punycode) {
|
||||
return Err(Error::InvalidBucketName(alias_name.to_string()));
|
||||
}
|
||||
|
||||
@@ -88,8 +96,7 @@ impl<'a> LockedHelper<'a> {
|
||||
// writes are now done and all writes use timestamp alias_ts
|
||||
|
||||
let alias = match alias {
|
||||
None => BucketAlias::new(alias_name.clone(), alias_ts, Some(bucket_id))
|
||||
.ok_or_else(|| Error::InvalidBucketName(alias_name.clone()))?,
|
||||
None => BucketAlias::new(alias_name.clone(), alias_ts, Some(bucket_id)),
|
||||
Some(mut a) => {
|
||||
a.state = Lww::raw(alias_ts, Some(bucket_id));
|
||||
a
|
||||
@@ -180,13 +187,14 @@ impl<'a> LockedHelper<'a> {
|
||||
.ok_or_else(|| Error::NoSuchBucket(alias_name.to_string()))?;
|
||||
|
||||
// Checks ok, remove alias
|
||||
let alias_ts = match bucket.state.as_option() {
|
||||
Some(bucket_state) => increment_logical_clock_2(
|
||||
alias.state.timestamp(),
|
||||
bucket_state.aliases.get_timestamp(alias_name),
|
||||
),
|
||||
None => increment_logical_clock(alias.state.timestamp()),
|
||||
};
|
||||
let alias_ts = increment_logical_clock_2(
|
||||
alias.state.timestamp(),
|
||||
bucket
|
||||
.state
|
||||
.as_option()
|
||||
.map(|p| p.aliases.get_timestamp(alias_name))
|
||||
.unwrap_or(0),
|
||||
);
|
||||
|
||||
// ---- timestamp-ensured causality barrier ----
|
||||
// writes are now done and all writes use timestamp alias_ts
|
||||
@@ -204,6 +212,10 @@ impl<'a> LockedHelper<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// local bucket aliases
|
||||
// ================================================
|
||||
|
||||
/// Sets a new alias for a bucket in the local namespace of a key.
|
||||
/// This function fails if:
|
||||
/// - alias name is not valid according to S3 spec
|
||||
@@ -216,14 +228,12 @@ impl<'a> LockedHelper<'a> {
|
||||
key_id: &String,
|
||||
alias_name: &String,
|
||||
) -> Result<(), Error> {
|
||||
let key_helper = KeyHelper(self.0);
|
||||
|
||||
if !is_valid_bucket_name(alias_name) {
|
||||
if !is_valid_bucket_name(alias_name, self.0.config.allow_punycode) {
|
||||
return Err(Error::InvalidBucketName(alias_name.to_string()));
|
||||
}
|
||||
|
||||
let mut bucket = self.bucket().get_existing_bucket(bucket_id).await?;
|
||||
let mut key = key_helper.get_existing_key(key_id).await?;
|
||||
let mut key = self.key().get_existing_key(key_id).await?;
|
||||
|
||||
let key_param = key.state.as_option_mut().unwrap();
|
||||
|
||||
@@ -272,23 +282,13 @@ impl<'a> LockedHelper<'a> {
|
||||
key_id: &String,
|
||||
alias_name: &String,
|
||||
) -> Result<(), Error> {
|
||||
let key_helper = KeyHelper(self.0);
|
||||
|
||||
let mut bucket = self.bucket().get_existing_bucket(bucket_id).await?;
|
||||
let mut key = key_helper.get_existing_key(key_id).await?;
|
||||
let mut key = self.key().get_existing_key(key_id).await?;
|
||||
|
||||
let key_p = key.state.as_option().unwrap();
|
||||
let bucket_p = bucket.state.as_option_mut().unwrap();
|
||||
|
||||
if key
|
||||
.state
|
||||
.as_option()
|
||||
.unwrap()
|
||||
.local_aliases
|
||||
.get(alias_name)
|
||||
.cloned()
|
||||
.flatten()
|
||||
!= Some(bucket_id)
|
||||
{
|
||||
if key_p.local_aliases.get(alias_name).cloned().flatten() != Some(bucket_id) {
|
||||
return Err(GarageError::Message(format!(
|
||||
"Bucket {:?} does not have alias {} in namespace of key {}",
|
||||
bucket_id, alias_name, key_id
|
||||
@@ -305,17 +305,17 @@ impl<'a> LockedHelper<'a> {
|
||||
.local_aliases
|
||||
.items()
|
||||
.iter()
|
||||
.any(|((k, n), _, active)| *k == key.key_id && n == alias_name && *active);
|
||||
.any(|((k, n), _, active)| (*k != key.key_id || n != alias_name) && *active);
|
||||
|
||||
if !has_other_global_aliases && !has_other_local_aliases {
|
||||
return Err(Error::BadRequest(format!("Bucket {} doesn't have other aliases, please delete it instead of just unaliasing.", alias_name)));
|
||||
}
|
||||
|
||||
// Checks ok, remove alias
|
||||
let key_param = key.state.as_option_mut().unwrap();
|
||||
let bucket_p_local_alias_key = (key.key_id.clone(), alias_name.clone());
|
||||
|
||||
let alias_ts = increment_logical_clock_2(
|
||||
key_param.local_aliases.get_timestamp(alias_name),
|
||||
key_p.local_aliases.get_timestamp(alias_name),
|
||||
bucket_p
|
||||
.local_aliases
|
||||
.get_timestamp(&bucket_p_local_alias_key),
|
||||
@@ -324,7 +324,8 @@ impl<'a> LockedHelper<'a> {
|
||||
// ---- timestamp-ensured causality barrier ----
|
||||
// writes are now done and all writes use timestamp alias_ts
|
||||
|
||||
key_param.local_aliases = LwwMap::raw_item(alias_name.clone(), alias_ts, None);
|
||||
key.state.as_option_mut().unwrap().local_aliases =
|
||||
LwwMap::raw_item(alias_name.clone(), alias_ts, None);
|
||||
self.0.key_table.insert(&key).await?;
|
||||
|
||||
bucket_p.local_aliases = LwwMap::raw_item(bucket_p_local_alias_key, alias_ts, false);
|
||||
@@ -333,21 +334,68 @@ impl<'a> LockedHelper<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensures a bucket does not have a certain local alias.
|
||||
/// Contrarily to unset_local_bucket_alias, this does not
|
||||
/// fail on any condition other than:
|
||||
/// - bucket cannot be found (its fine if it is in deleted state)
|
||||
/// - key cannot be found (its fine if alias in key points to nothing
|
||||
/// or to another bucket)
|
||||
pub async fn purge_local_bucket_alias(
|
||||
&self,
|
||||
bucket_id: Uuid,
|
||||
key_id: &String,
|
||||
alias_name: &String,
|
||||
) -> Result<(), Error> {
|
||||
let mut bucket = self.bucket().get_internal_bucket(bucket_id).await?;
|
||||
let mut key = self.key().get_internal_key(key_id).await?;
|
||||
|
||||
let bucket_p_local_alias_key = (key.key_id.clone(), alias_name.clone());
|
||||
|
||||
let alias_ts = increment_logical_clock_2(
|
||||
key.state
|
||||
.as_option()
|
||||
.map(|p| p.local_aliases.get_timestamp(alias_name))
|
||||
.unwrap_or(0),
|
||||
bucket
|
||||
.state
|
||||
.as_option()
|
||||
.map(|p| p.local_aliases.get_timestamp(&bucket_p_local_alias_key))
|
||||
.unwrap_or(0),
|
||||
);
|
||||
|
||||
// ---- timestamp-ensured causality barrier ----
|
||||
// writes are now done and all writes use timestamp alias_ts
|
||||
|
||||
if let Some(kp) = key.state.as_option_mut() {
|
||||
kp.local_aliases = LwwMap::raw_item(alias_name.clone(), alias_ts, None);
|
||||
self.0.key_table.insert(&key).await?;
|
||||
}
|
||||
|
||||
if let Some(bp) = bucket.state.as_option_mut() {
|
||||
bp.local_aliases = LwwMap::raw_item(bucket_p_local_alias_key, alias_ts, false);
|
||||
self.0.bucket_table.insert(&bucket).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// permissions
|
||||
// ================================================
|
||||
|
||||
/// Sets permissions for a key on a bucket.
|
||||
/// This function fails if:
|
||||
/// - bucket or key cannot be found at all (its ok if they are in deleted state)
|
||||
/// - bucket or key is in deleted state and we are trying to set permissions other than "deny
|
||||
/// all"
|
||||
/// - bucket or key is in deleted state and we are trying to set
|
||||
/// permissions other than "deny all"
|
||||
pub async fn set_bucket_key_permissions(
|
||||
&self,
|
||||
bucket_id: Uuid,
|
||||
key_id: &String,
|
||||
mut perm: BucketKeyPerm,
|
||||
) -> Result<(), Error> {
|
||||
let key_helper = KeyHelper(self.0);
|
||||
|
||||
let mut bucket = self.bucket().get_internal_bucket(bucket_id).await?;
|
||||
let mut key = key_helper.get_internal_key(key_id).await?;
|
||||
let mut key = self.key().get_internal_key(key_id).await?;
|
||||
|
||||
if let Some(bstate) = bucket.state.as_option() {
|
||||
if let Some(kp) = bstate.authorized_keys.get(key_id) {
|
||||
@@ -384,21 +432,20 @@ impl<'a> LockedHelper<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ----
|
||||
// ================================================
|
||||
// keys
|
||||
// ================================================
|
||||
|
||||
/// Deletes an API access key
|
||||
pub async fn delete_key(&self, key: &mut Key) -> Result<(), Error> {
|
||||
let state = key.state.as_option_mut().unwrap();
|
||||
|
||||
// --- done checking, now commit ---
|
||||
// (the step at unset_local_bucket_alias will fail if a bucket
|
||||
// does not have another alias, the deletion will be
|
||||
// interrupted in the middle if that happens)
|
||||
|
||||
// 1. Delete local aliases
|
||||
for (alias, _, to) in state.local_aliases.items().iter() {
|
||||
if let Some(bucket_id) = to {
|
||||
self.unset_local_bucket_alias(*bucket_id, &key.key_id, alias)
|
||||
self.purge_local_bucket_alias(*bucket_id, &key.key_id, alias)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@@ -415,4 +462,193 @@ impl<'a> LockedHelper<'a> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// repair procedure
|
||||
// ================================================
|
||||
|
||||
pub async fn repair_aliases(&self) -> Result<(), GarageError> {
|
||||
self.0.db.transaction(|tx| {
|
||||
info!("--- begin repair_aliases transaction ----");
|
||||
|
||||
// 1. List all non-deleted buckets, so that we can fix bad aliases
|
||||
let mut all_buckets: HashSet<Uuid> = HashSet::new();
|
||||
|
||||
for item in tx.range::<&[u8], _>(&self.0.bucket_table.data.store, ..)? {
|
||||
let bucket = self
|
||||
.0
|
||||
.bucket_table
|
||||
.data
|
||||
.decode_entry(&(item?.1))
|
||||
.map_err(db::TxError::Abort)?;
|
||||
if !bucket.is_deleted() {
|
||||
all_buckets.insert(bucket.id);
|
||||
}
|
||||
}
|
||||
|
||||
info!("number of buckets: {}", all_buckets.len());
|
||||
|
||||
// 2. List all aliases declared in bucket_alias_table and key_table
|
||||
// Take note of aliases that point to non-existing buckets
|
||||
let mut global_aliases: HashMap<String, Uuid> = HashMap::new();
|
||||
|
||||
{
|
||||
let mut delete_global = vec![];
|
||||
for item in tx.range::<&[u8], _>(&self.0.bucket_alias_table.data.store, ..)? {
|
||||
let mut alias = self
|
||||
.0
|
||||
.bucket_alias_table
|
||||
.data
|
||||
.decode_entry(&(item?.1))
|
||||
.map_err(db::TxError::Abort)?;
|
||||
if let Some(id) = alias.state.get() {
|
||||
if all_buckets.contains(id) {
|
||||
// keep aliases
|
||||
global_aliases.insert(alias.name().to_string(), *id);
|
||||
} else {
|
||||
// delete alias
|
||||
warn!(
|
||||
"global alias: remove {} -> {:?} (bucket is deleted)",
|
||||
alias.name(),
|
||||
id
|
||||
);
|
||||
alias.state.update(None);
|
||||
delete_global.push(alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("number of global aliases: {}", global_aliases.len());
|
||||
|
||||
info!("global alias table: {} entries fixed", delete_global.len());
|
||||
for ga in delete_global {
|
||||
debug!("Enqueue update to global alias table: {:?}", ga);
|
||||
self.0.bucket_alias_table.queue_insert(tx, &ga)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut local_aliases: HashMap<(String, String), Uuid> = HashMap::new();
|
||||
|
||||
{
|
||||
let mut delete_local = vec![];
|
||||
|
||||
for item in tx.range::<&[u8], _>(&self.0.key_table.data.store, ..)? {
|
||||
let mut key = self
|
||||
.0
|
||||
.key_table
|
||||
.data
|
||||
.decode_entry(&(item?.1))
|
||||
.map_err(db::TxError::Abort)?;
|
||||
let Some(p) = key.state.as_option_mut() else {
|
||||
continue;
|
||||
};
|
||||
let mut has_changes = false;
|
||||
for (name, _, to) in p.local_aliases.items().to_vec() {
|
||||
if let Some(id) = to {
|
||||
if all_buckets.contains(&id) {
|
||||
local_aliases.insert((key.key_id.clone(), name), id);
|
||||
} else {
|
||||
warn!(
|
||||
"local alias: remove ({}, {}) -> {:?} (bucket is deleted)",
|
||||
key.key_id, name, id
|
||||
);
|
||||
p.local_aliases.update_in_place(name, None);
|
||||
has_changes = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if has_changes {
|
||||
delete_local.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
info!("number of local aliases: {}", local_aliases.len());
|
||||
|
||||
info!("key table: {} entries fixed", delete_local.len());
|
||||
for la in delete_local {
|
||||
debug!("Enqueue update to key table: {:?}", la);
|
||||
self.0.key_table.queue_insert(tx, &la)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Reverse the alias maps to determine the aliases per-bucket
|
||||
let mut bucket_global: HashMap<Uuid, Vec<String>> = HashMap::new();
|
||||
let mut bucket_local: HashMap<Uuid, Vec<(String, String)>> = HashMap::new();
|
||||
|
||||
for (name, bucket) in global_aliases {
|
||||
bucket_global.entry(bucket).or_default().push(name);
|
||||
}
|
||||
for ((key, name), bucket) in local_aliases {
|
||||
bucket_local.entry(bucket).or_default().push((key, name));
|
||||
}
|
||||
|
||||
// 5. Fix the bucket table to ensure consistency
|
||||
let mut bucket_updates = vec![];
|
||||
|
||||
for item in tx.range::<&[u8], _>(&self.0.bucket_table.data.store, ..)? {
|
||||
let bucket = self
|
||||
.0
|
||||
.bucket_table
|
||||
.data
|
||||
.decode_entry(&(item?.1))
|
||||
.map_err(db::TxError::Abort)?;
|
||||
let mut bucket2 = bucket.clone();
|
||||
let Some(param) = bucket2.state.as_option_mut() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// fix global aliases
|
||||
{
|
||||
let ga = bucket_global.remove(&bucket.id).unwrap_or_default();
|
||||
for (name, _, active) in param.aliases.items().to_vec() {
|
||||
if active && !ga.contains(&name) {
|
||||
warn!("bucket {:?}: remove global alias {}", bucket.id, name);
|
||||
param.aliases.update_in_place(name, false);
|
||||
}
|
||||
}
|
||||
for name in ga {
|
||||
if param.aliases.get(&name).copied() != Some(true) {
|
||||
warn!("bucket {:?}: add global alias {}", bucket.id, name);
|
||||
param.aliases.update_in_place(name, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fix local aliases
|
||||
{
|
||||
let la = bucket_local.remove(&bucket.id).unwrap_or_default();
|
||||
for (pair, _, active) in param.local_aliases.items().to_vec() {
|
||||
if active && !la.contains(&pair) {
|
||||
warn!("bucket {:?}: remove local alias {:?}", bucket.id, pair);
|
||||
param.local_aliases.update_in_place(pair, false);
|
||||
}
|
||||
}
|
||||
for pair in la {
|
||||
if param.local_aliases.get(&pair).copied() != Some(true) {
|
||||
warn!("bucket {:?}: add local alias {:?}", bucket.id, pair);
|
||||
param.local_aliases.update_in_place(pair, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bucket2 != bucket {
|
||||
bucket_updates.push(bucket2);
|
||||
}
|
||||
}
|
||||
|
||||
info!("bucket table: {} entries fixed", bucket_updates.len());
|
||||
for b in bucket_updates {
|
||||
debug!("Enqueue update to bucket table: {:?}", b);
|
||||
self.0.bucket_table.queue_insert(tx, &b)?;
|
||||
}
|
||||
|
||||
info!("--- end repair_aliases transaction ----");
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
info!("repair_aliases is done");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use garage_db as db;
|
||||
use garage_rpc::layout::LayoutHelper;
|
||||
use garage_rpc::system::System;
|
||||
use garage_util::background::BackgroundRunner;
|
||||
use garage_util::config::Config;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::*;
|
||||
use garage_util::migrate::Migrate;
|
||||
@@ -174,7 +173,6 @@ impl<T: CountedItem> IndexCounter<T> {
|
||||
system: Arc<System>,
|
||||
replication: TableShardedReplication,
|
||||
db: &db::Db,
|
||||
config: &Config,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
this_node: system.id,
|
||||
@@ -188,7 +186,6 @@ impl<T: CountedItem> IndexCounter<T> {
|
||||
replication,
|
||||
system,
|
||||
db,
|
||||
&config.experimental.merkle_backpressure,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_net"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -39,7 +39,6 @@ kuska-handshake.workspace = true
|
||||
|
||||
opentelemetry = { workspace = true, optional = true }
|
||||
opentelemetry-contrib = { workspace = true, optional = true }
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_env_logger.workspace = true
|
||||
|
||||
+1
-22
@@ -4,7 +4,6 @@ use std::pin::Pin;
|
||||
use std::sync::atomic::{self, AtomicU32};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::Poll;
|
||||
use tracing::*;
|
||||
|
||||
use arc_swap::ArcSwapOption;
|
||||
use bytes::Bytes;
|
||||
@@ -15,7 +14,7 @@ use futures::Stream;
|
||||
use kuska_handshake::async_std::{handshake_client, BoxStream};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::select;
|
||||
use tokio::sync::{mpsc, oneshot, watch, Semaphore};
|
||||
use tokio::sync::{mpsc, oneshot, watch};
|
||||
use tokio_util::compat::*;
|
||||
|
||||
#[cfg(feature = "telemetry")]
|
||||
@@ -26,7 +25,6 @@ use opentelemetry::{
|
||||
#[cfg(feature = "telemetry")]
|
||||
use opentelemetry_contrib::trace::propagator::binary::*;
|
||||
|
||||
use crate::endpoint::RpcInFlightLimiter;
|
||||
use crate::error::*;
|
||||
use crate::message::*;
|
||||
use crate::netapp::*;
|
||||
@@ -43,7 +41,6 @@ pub(crate) struct ClientConn {
|
||||
|
||||
next_query_number: AtomicU32,
|
||||
inflight: Mutex<HashMap<RequestID, oneshot::Sender<ByteStream>>>,
|
||||
rpc_table_write_inflight_limiter: Option<Semaphore>,
|
||||
}
|
||||
|
||||
impl ClientConn {
|
||||
@@ -101,14 +98,8 @@ impl ClientConn {
|
||||
next_query_number: AtomicU32::from(RequestID::default()),
|
||||
query_send: ArcSwapOption::new(Some(Arc::new(query_send))),
|
||||
inflight: Mutex::new(HashMap::new()),
|
||||
rpc_table_write_inflight_limiter: netapp.max_in_flight_table_write.map(Semaphore::new),
|
||||
});
|
||||
|
||||
info!(
|
||||
"Created conn with table write limit set to {}",
|
||||
netapp.max_in_flight_table_write.unwrap_or(0)
|
||||
);
|
||||
|
||||
netapp.connected_as_client(peer_id, conn.clone());
|
||||
|
||||
let debug_name = format!("CLI {}", hex::encode(&peer_id[..8]));
|
||||
@@ -153,21 +144,10 @@ impl ClientConn {
|
||||
req: Req<T>,
|
||||
path: &str,
|
||||
prio: RequestPriority,
|
||||
limiter: RpcInFlightLimiter,
|
||||
) -> Result<Resp<T>, Error>
|
||||
where
|
||||
T: Message,
|
||||
{
|
||||
let _permit = match (limiter, &self.rpc_table_write_inflight_limiter) {
|
||||
(RpcInFlightLimiter::TableWrite, Some(sem)) => {
|
||||
info!(
|
||||
"Available RPC table write slots: {}",
|
||||
sem.available_permits()
|
||||
);
|
||||
Some(sem.acquire().await.unwrap())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let query_send = self.query_send.load_full().ok_or(Error::ConnectionClosed)?;
|
||||
|
||||
let id = self
|
||||
@@ -232,7 +212,6 @@ impl ClientConn {
|
||||
let stream = Box::pin(canceller.for_stream(stream));
|
||||
|
||||
let resp_enc = RespEnc::decode(stream).await?;
|
||||
drop(_permit);
|
||||
debug!("client: got response to request {} (path {})", id, path);
|
||||
Resp::from_enc(resp_enc)
|
||||
}
|
||||
|
||||
+2
-17
@@ -57,13 +57,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub enum RpcInFlightLimiter {
|
||||
#[default]
|
||||
NoLimit,
|
||||
TableWrite,
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
/// This struct represents an endpoint for message of type `M`.
|
||||
@@ -121,7 +114,6 @@ where
|
||||
target: &NodeID,
|
||||
req: T,
|
||||
prio: RequestPriority,
|
||||
limiter: RpcInFlightLimiter,
|
||||
) -> Result<Resp<M>, Error>
|
||||
where
|
||||
T: IntoReq<M>,
|
||||
@@ -144,10 +136,7 @@ where
|
||||
"Not connected: {}",
|
||||
hex::encode(&target[..8])
|
||||
))),
|
||||
Some(c) => {
|
||||
c.call(req.into_req()?, self.path.as_str(), prio, limiter)
|
||||
.await
|
||||
}
|
||||
Some(c) => c.call(req.into_req()?, self.path.as_str(), prio).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,12 +149,8 @@ where
|
||||
target: &NodeID,
|
||||
req: M,
|
||||
prio: RequestPriority,
|
||||
limiter: RpcInFlightLimiter,
|
||||
) -> Result<<M as Message>::Response, Error> {
|
||||
Ok(self
|
||||
.call_streaming(target, req, prio, limiter)
|
||||
.await?
|
||||
.into_msg())
|
||||
Ok(self.call_streaming(target, req, prio).await?.into_msg())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,8 +74,6 @@ pub struct NetApp {
|
||||
pub id: NodeID,
|
||||
/// Private key associated with our peer ID
|
||||
pub privkey: ed25519::SecretKey,
|
||||
/// Config related to netapp
|
||||
pub(crate) max_in_flight_table_write: Option<usize>,
|
||||
|
||||
pub(crate) server_conns: RwLock<HashMap<NodeID, Arc<ServerConn>>>,
|
||||
pub(crate) client_conns: RwLock<HashMap<NodeID, Arc<ClientConn>>>,
|
||||
@@ -103,7 +101,6 @@ impl NetApp {
|
||||
netid: auth::Key,
|
||||
privkey: ed25519::SecretKey,
|
||||
bind_outgoing_to: Option<IpAddr>,
|
||||
max_in_flight_table_write: Option<usize>,
|
||||
) -> Arc<Self> {
|
||||
let mut version_tag = [0u8; 16];
|
||||
version_tag[0..8].copy_from_slice(&u64::to_be_bytes(NETAPP_VERSION_TAG)[..]);
|
||||
@@ -117,7 +114,6 @@ impl NetApp {
|
||||
netid,
|
||||
id,
|
||||
privkey,
|
||||
max_in_flight_table_write,
|
||||
server_conns: RwLock::new(HashMap::new()),
|
||||
client_conns: RwLock::new(HashMap::new()),
|
||||
endpoints: RwLock::new(HashMap::new()),
|
||||
@@ -431,7 +427,6 @@ impl NetApp {
|
||||
server_port,
|
||||
},
|
||||
PRIO_NORMAL,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
|
||||
+2
-7
@@ -406,7 +406,7 @@ impl PeeringManager {
|
||||
ping_time
|
||||
);
|
||||
let ping_response = select! {
|
||||
r = self.ping_endpoint.call(&id, ping_msg, PRIO_HIGH, RpcInFlightLimiter::NoLimit) => r,
|
||||
r = self.ping_endpoint.call(&id, ping_msg, PRIO_HIGH) => r,
|
||||
_ = tokio::time::sleep(ping_timeout) => Err(Error::Message("Ping timeout".into())),
|
||||
};
|
||||
|
||||
@@ -458,12 +458,7 @@ impl PeeringManager {
|
||||
let pex_message = PeerListMessage { list: peer_list };
|
||||
match self
|
||||
.peer_list_endpoint
|
||||
.call(
|
||||
id,
|
||||
pex_message,
|
||||
PRIO_BACKGROUND,
|
||||
RpcInFlightLimiter::NoLimit,
|
||||
)
|
||||
.call(id, pex_message, PRIO_BACKGROUND)
|
||||
.await
|
||||
{
|
||||
Err(e) => warn!("Error doing peer exchange: {}", e),
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_rpc"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
+1
-12
@@ -6,7 +6,6 @@ use std::time::Duration;
|
||||
use futures::future::join_all;
|
||||
use futures::stream::futures_unordered::FuturesUnordered;
|
||||
use futures::stream::StreamExt;
|
||||
use garage_net::endpoint::RpcInFlightLimiter;
|
||||
use tokio::select;
|
||||
|
||||
use opentelemetry::KeyValue;
|
||||
@@ -45,8 +44,6 @@ pub struct RequestStrategy<T> {
|
||||
rs_timeout: Timeout,
|
||||
/// Data to drop when everything completes
|
||||
rs_drop_on_complete: T,
|
||||
/// RPC In Flight Limiter
|
||||
rs_inflight_limiter: RpcInFlightLimiter,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -64,7 +61,6 @@ impl Clone for RequestStrategy<()> {
|
||||
rs_priority: self.rs_priority,
|
||||
rs_timeout: self.rs_timeout,
|
||||
rs_drop_on_complete: (),
|
||||
rs_inflight_limiter: self.rs_inflight_limiter,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +74,6 @@ impl RequestStrategy<()> {
|
||||
rs_priority: prio,
|
||||
rs_timeout: Timeout::Default,
|
||||
rs_drop_on_complete: (),
|
||||
rs_inflight_limiter: RpcInFlightLimiter::NoLimit,
|
||||
}
|
||||
}
|
||||
/// Add an item to be dropped on completion
|
||||
@@ -89,7 +84,6 @@ impl RequestStrategy<()> {
|
||||
rs_priority: self.rs_priority,
|
||||
rs_timeout: self.rs_timeout,
|
||||
rs_drop_on_complete: drop_on_complete,
|
||||
rs_inflight_limiter: RpcInFlightLimiter::NoLimit,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,10 +109,6 @@ impl<T> RequestStrategy<T> {
|
||||
self.rs_timeout = Timeout::Custom(timeout);
|
||||
self
|
||||
}
|
||||
pub fn with_write_limiter(mut self) -> Self {
|
||||
self.rs_inflight_limiter = RpcInFlightLimiter::TableWrite;
|
||||
self
|
||||
}
|
||||
/// Extract drop_on_complete item
|
||||
fn extract_drop_on_complete(self) -> (RequestStrategy<()>, T) {
|
||||
(
|
||||
@@ -128,7 +118,6 @@ impl<T> RequestStrategy<T> {
|
||||
rs_priority: self.rs_priority,
|
||||
rs_timeout: self.rs_timeout,
|
||||
rs_drop_on_complete: (),
|
||||
rs_inflight_limiter: self.rs_inflight_limiter,
|
||||
},
|
||||
self.rs_drop_on_complete,
|
||||
)
|
||||
@@ -196,7 +185,7 @@ impl RpcHelper {
|
||||
|
||||
let node_id = to.into();
|
||||
let rpc_call = endpoint
|
||||
.call_streaming(&node_id, msg, strat.rs_priority, strat.rs_inflight_limiter)
|
||||
.call_streaming(&node_id, msg, strat.rs_priority)
|
||||
.with_context(Context::current_with_span(span))
|
||||
.record_duration(&self.0.metrics.rpc_duration, &metric_tags);
|
||||
|
||||
|
||||
+2
-12
@@ -21,7 +21,7 @@ use garage_net::{NetApp, NetworkKey, NodeID, NodeKey};
|
||||
|
||||
#[cfg(feature = "kubernetes-discovery")]
|
||||
use garage_util::config::KubernetesDiscoveryConfig;
|
||||
use garage_util::config::{Config, DataDirEnum, RpcInFlightLimiterEnum};
|
||||
use garage_util::config::{Config, DataDirEnum};
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::*;
|
||||
use garage_util::persister::Persister;
|
||||
@@ -256,17 +256,7 @@ impl System {
|
||||
let bind_outgoing_to = Some(config)
|
||||
.filter(|x| x.rpc_bind_outgoing)
|
||||
.map(|x| x.rpc_bind_addr.ip());
|
||||
let maybe_max_table_write = match &config.experimental.rpc_in_flight_limiters {
|
||||
RpcInFlightLimiterEnum::None => None,
|
||||
RpcInFlightLimiterEnum::FixedSize(v) => Some(v.max_table_write),
|
||||
};
|
||||
let netapp = NetApp::new(
|
||||
GARAGE_VERSION_TAG,
|
||||
network_key,
|
||||
node_key,
|
||||
bind_outgoing_to,
|
||||
maybe_max_table_write,
|
||||
);
|
||||
let netapp = NetApp::new(GARAGE_VERSION_TAG, network_key, node_key, bind_outgoing_to);
|
||||
let system_endpoint = netapp.endpoint(SYSTEM_RPC_PATH.into());
|
||||
|
||||
// ---- setup netapp public listener and full mesh peering strategy ----
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_table"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
+35
-133
@@ -3,12 +3,10 @@ use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_bytes::ByteBuf;
|
||||
use tokio::sync::SemaphorePermit;
|
||||
use tokio::sync::{Notify, Semaphore};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use garage_db as db;
|
||||
|
||||
use garage_util::config::MerkleBackpressureEnum;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::*;
|
||||
use garage_util::migrate::Migrate;
|
||||
@@ -22,67 +20,6 @@ use crate::replication::*;
|
||||
use crate::schema::*;
|
||||
use crate::util::*;
|
||||
|
||||
pub(crate) struct MerkleTodo {
|
||||
merkle_todo: db::Tree,
|
||||
merkle_todo_notify: Notify,
|
||||
merkle_todo_bounded_queue: Option<Arc<Semaphore>>,
|
||||
}
|
||||
impl Clone for MerkleTodo {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
merkle_todo: self.merkle_todo.clone(),
|
||||
merkle_todo_notify: Notify::new(),
|
||||
merkle_todo_bounded_queue: self.merkle_todo_bounded_queue.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MerkleTodo {
|
||||
fn new<F: TableSchema>(db: &db::Db, config: &MerkleBackpressureEnum) -> Self {
|
||||
let merkle_todo = db
|
||||
.open_tree(format!("{}:merkle_todo", F::TABLE_NAME))
|
||||
.expect("Unable to open DB Merkle TODO tree");
|
||||
|
||||
let merkle_todo_bounded_queue = match config {
|
||||
MerkleBackpressureEnum::None => None,
|
||||
MerkleBackpressureEnum::FixedQueue(p) => {
|
||||
Some(Arc::new(Semaphore::new(p.max_queue_size)))
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
merkle_todo,
|
||||
merkle_todo_notify: Notify::new(),
|
||||
merkle_todo_bounded_queue,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> Result<usize, db::Error> {
|
||||
self.merkle_todo.len()
|
||||
}
|
||||
|
||||
pub(crate) async fn with_db<F: FnOnce(&db::Tree, SemaphorePermit)>(&self, f: F) {
|
||||
let bounded = self
|
||||
.merkle_todo_bounded_queue
|
||||
.clone()
|
||||
.unwrap_or(Arc::new(Semaphore::new(1)));
|
||||
let permit = bounded.acquire().await.unwrap();
|
||||
f(&self.merkle_todo, permit);
|
||||
}
|
||||
|
||||
pub(crate) fn appended(&self, permit: SemaphorePermit) {
|
||||
permit.forget();
|
||||
self.merkle_todo_notify.notify_one();
|
||||
}
|
||||
|
||||
pub(crate) fn processed(&self) {
|
||||
let bounded = self
|
||||
.merkle_todo_bounded_queue
|
||||
.clone()
|
||||
.unwrap_or(Arc::new(Semaphore::new(1)));
|
||||
bounded.add_permits(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TableData<F: TableSchema, R: TableReplication> {
|
||||
system: Arc<System>,
|
||||
|
||||
@@ -92,7 +29,8 @@ pub struct TableData<F: TableSchema, R: TableReplication> {
|
||||
pub store: db::Tree,
|
||||
|
||||
pub(crate) merkle_tree: db::Tree,
|
||||
pub(crate) merkle_todo: MerkleTodo,
|
||||
pub(crate) merkle_todo: db::Tree,
|
||||
pub(crate) merkle_todo_notify: Notify,
|
||||
|
||||
pub(crate) insert_queue: db::Tree,
|
||||
pub(crate) insert_queue_notify: Arc<Notify>,
|
||||
@@ -100,18 +38,10 @@ pub struct TableData<F: TableSchema, R: TableReplication> {
|
||||
pub(crate) gc_todo: db::Tree,
|
||||
|
||||
pub(crate) metrics: TableMetrics,
|
||||
|
||||
pub(crate) config: MerkleBackpressureEnum,
|
||||
}
|
||||
|
||||
impl<F: TableSchema, R: TableReplication> TableData<F, R> {
|
||||
pub fn new(
|
||||
system: Arc<System>,
|
||||
instance: F,
|
||||
replication: R,
|
||||
db: &db::Db,
|
||||
config: &MerkleBackpressureEnum,
|
||||
) -> Arc<Self> {
|
||||
pub fn new(system: Arc<System>, instance: F, replication: R, db: &db::Db) -> Arc<Self> {
|
||||
let store = db
|
||||
.open_tree(format!("{}:table", F::TABLE_NAME))
|
||||
.expect("Unable to open DB tree");
|
||||
@@ -119,8 +49,9 @@ impl<F: TableSchema, R: TableReplication> TableData<F, R> {
|
||||
let merkle_tree = db
|
||||
.open_tree(format!("{}:merkle_tree", F::TABLE_NAME))
|
||||
.expect("Unable to open DB Merkle tree tree");
|
||||
|
||||
let merkle_todo = MerkleTodo::new::<F>(db, config);
|
||||
let merkle_todo = db
|
||||
.open_tree(format!("{}:merkle_todo", F::TABLE_NAME))
|
||||
.expect("Unable to open DB Merkle TODO tree");
|
||||
|
||||
let insert_queue = db
|
||||
.open_tree(format!("{}:insert_queue", F::TABLE_NAME))
|
||||
@@ -145,11 +76,11 @@ impl<F: TableSchema, R: TableReplication> TableData<F, R> {
|
||||
store,
|
||||
merkle_tree,
|
||||
merkle_todo,
|
||||
merkle_todo_notify: Notify::new(),
|
||||
insert_queue,
|
||||
insert_queue_notify: Arc::new(Notify::new()),
|
||||
gc_todo,
|
||||
metrics,
|
||||
config: config.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -236,8 +167,6 @@ impl<F: TableSchema, R: TableReplication> TableData<F, R> {
|
||||
// - When an entry is modified or deleted, add it to the merkle updater's todo list.
|
||||
// This has to be done atomically with the modification for the merkle updater
|
||||
// to maintain consistency. The merkle updater must then be notified with todo_notify.
|
||||
// Also to avoid overloading the merkle updater, you need to sleep a given amount of
|
||||
// time to enable backpressure (ie. slow down clients).
|
||||
// - When an entry is updated to be a tombstone, add it to the gc_todo tree
|
||||
|
||||
pub(crate) fn update_many<T: Borrow<ByteBuf>>(&self, entries: &[T]) -> Result<(), Error> {
|
||||
@@ -272,7 +201,6 @@ impl<F: TableSchema, R: TableReplication> TableData<F, R> {
|
||||
) -> Result<Option<F::E>, Error> {
|
||||
let tree_key = self.tree_key(partition_key, sort_key);
|
||||
|
||||
// transaction begins
|
||||
let changed = self.store.db().transaction(|tx| {
|
||||
let (old_entry, old_bytes, new_entry) = match tx.get(&self.store, &tree_key)? {
|
||||
Some(old_bytes) => {
|
||||
@@ -310,44 +238,31 @@ impl<F: TableSchema, R: TableReplication> TableData<F, R> {
|
||||
Ok(None)
|
||||
}
|
||||
})?;
|
||||
// transaction ends
|
||||
|
||||
// early return if nothing changed
|
||||
let (new_entry, new_bytes_hash) = match changed {
|
||||
Some((e, b)) => (e, b),
|
||||
None => {
|
||||
let maybe_bound = self.merkle_todo_bounded_queue.clone();
|
||||
if let Some(b) = &maybe_bound {
|
||||
b.add_permits(1);
|
||||
if let Some((new_entry, new_bytes_hash)) = changed {
|
||||
self.metrics.internal_update_counter.add(1);
|
||||
|
||||
let is_tombstone = new_entry.is_tombstone();
|
||||
self.merkle_todo_notify.notify_one();
|
||||
if is_tombstone {
|
||||
// We are only responsible for GC'ing this item if we are the
|
||||
// "leader" of the partition, i.e. the first node in the
|
||||
// set of nodes that replicates this partition.
|
||||
// This avoids GC loops and does not change the termination properties
|
||||
// of the GC algorithm, as in all cases GC is suspended if
|
||||
// any node of the partition is unavailable.
|
||||
let pk_hash = Hash::try_from(&tree_key[..32]).unwrap();
|
||||
// TODO: this probably breaks when the layout changes
|
||||
let nodes = self.replication.storage_nodes(&pk_hash);
|
||||
if nodes.first() == Some(&self.system.id) {
|
||||
GcTodoEntry::new(tree_key, new_bytes_hash).save(&self.gc_todo)?;
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle GC in case of tombstone
|
||||
let is_tombstone = new_entry.is_tombstone();
|
||||
if is_tombstone {
|
||||
// We are only responsible for GC'ing this item if we are the
|
||||
// "leader" of the partition, i.e. the first node in the
|
||||
// set of nodes that replicates this partition.
|
||||
// This avoids GC loops and does not change the termination properties
|
||||
// of the GC algorithm, as in all cases GC is suspended if
|
||||
// any node of the partition is unavailable.
|
||||
let pk_hash = Hash::try_from(&tree_key[..32]).unwrap();
|
||||
// TODO: this probably breaks when the layout changes
|
||||
let nodes = self.replication.storage_nodes(&pk_hash);
|
||||
if nodes.first() == Some(&self.system.id) {
|
||||
GcTodoEntry::new(tree_key, new_bytes_hash).save(&self.gc_todo)?;
|
||||
}
|
||||
Ok(Some(new_entry))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Collect metrics
|
||||
self.metrics.internal_update_counter.add(1);
|
||||
|
||||
// Synchronize with the Merkle Worker
|
||||
self.merkle_todo_notify.notify_one(); // Wake-up it
|
||||
|
||||
Ok(Some(new_entry))
|
||||
}
|
||||
|
||||
pub(crate) fn delete_if_equal(self: &Arc<Self>, k: &[u8], v: &[u8]) -> Result<bool, Error> {
|
||||
@@ -367,16 +282,10 @@ impl<F: TableSchema, R: TableReplication> TableData<F, R> {
|
||||
_ => Ok(false),
|
||||
})?;
|
||||
|
||||
if !removed {
|
||||
let maybe_bound = self.merkle_todo_bounded_queue.clone();
|
||||
if let Some(b) = &maybe_bound {
|
||||
b.add_permits(1);
|
||||
}
|
||||
return Ok(false);
|
||||
if removed {
|
||||
self.metrics.internal_delete_counter.add(1);
|
||||
self.merkle_todo_notify.notify_one();
|
||||
}
|
||||
|
||||
self.metrics.internal_delete_counter.add(1);
|
||||
self.merkle_todo_notify.notify_one();
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
@@ -401,18 +310,11 @@ impl<F: TableSchema, R: TableReplication> TableData<F, R> {
|
||||
_ => Ok(false),
|
||||
})?;
|
||||
|
||||
if !removed {
|
||||
let maybe_bound = self.merkle_todo_bounded_queue.clone();
|
||||
if let Some(b) = &maybe_bound {
|
||||
b.add_permits(1);
|
||||
}
|
||||
return Ok(false);
|
||||
if removed {
|
||||
self.metrics.internal_delete_counter.add(1);
|
||||
self.merkle_todo_notify.notify_one();
|
||||
}
|
||||
|
||||
self.metrics.internal_delete_counter.add(1);
|
||||
self.merkle_todo_notify.notify_one();
|
||||
|
||||
Ok(true)
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
// ---- Insert queue functions ----
|
||||
|
||||
+3
-10
@@ -262,8 +262,7 @@ impl<F: TableSchema, R: TableReplication> TableGc<F, R> {
|
||||
// GC has been successful for all of these entries.
|
||||
// We now remove them all from our local table and from the GC todo list.
|
||||
for item in items {
|
||||
let _is_removed = self
|
||||
.data
|
||||
self.data
|
||||
.delete_if_equal_hash(&item.key[..], item.value_hash)
|
||||
.err_context("GC: local delete tombstones")?;
|
||||
item.remove_if_equal(&self.data.gc_todo)
|
||||
@@ -276,21 +275,14 @@ impl<F: TableSchema, R: TableReplication> TableGc<F, R> {
|
||||
|
||||
impl<F: TableSchema, R: TableReplication> EndpointHandler<GcRpc> for TableGc<F, R> {
|
||||
async fn handle(self: &Arc<Self>, message: &GcRpc, _from: NodeID) -> Result<GcRpc, Error> {
|
||||
let maybe_bounded = self.data.merkle_todo_bounded_queue.clone();
|
||||
match message {
|
||||
GcRpc::Update(items) => {
|
||||
if let Some(b) = maybe_bounded {
|
||||
b.acquire_many(items.len() as u32).await.unwrap().forget();
|
||||
}
|
||||
self.data.update_many(items)?;
|
||||
Ok(GcRpc::Ok)
|
||||
}
|
||||
GcRpc::DeleteIfEqualHash(items) => {
|
||||
if let Some(b) = maybe_bounded {
|
||||
b.acquire_many(items.len() as u32).await.unwrap().forget();
|
||||
}
|
||||
for (key, vhash) in items.iter() {
|
||||
let _is_removed = self.data.delete_if_equal_hash(&key[..], *vhash)?;
|
||||
self.data.delete_if_equal_hash(&key[..], *vhash)?;
|
||||
}
|
||||
Ok(GcRpc::Ok)
|
||||
}
|
||||
@@ -337,6 +329,7 @@ impl<F: TableSchema, R: TableReplication> Worker for GcWorker<F, R> {
|
||||
}
|
||||
|
||||
async fn wait_for_work(&mut self) -> WorkerState {
|
||||
tokio::time::sleep(self.wait_delay).await;
|
||||
WorkerState::Busy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use tokio::sync::watch;
|
||||
use garage_db as db;
|
||||
|
||||
use garage_util::background::*;
|
||||
use garage_util::config::MerkleBackpressureEnum;
|
||||
use garage_util::data::*;
|
||||
use garage_util::encode::{nonversioned_decode, nonversioned_encode};
|
||||
use garage_util::error::Error;
|
||||
@@ -71,15 +70,6 @@ impl<F: TableSchema, R: TableReplication> MerkleUpdater<F, R> {
|
||||
pub(crate) fn new(data: Arc<TableData<F, R>>) -> Arc<Self> {
|
||||
let empty_node_hash = blake2sum(&nonversioned_encode(&MerkleNode::Empty).unwrap()[..]);
|
||||
|
||||
// @FIXME: move in worker
|
||||
match &data.config {
|
||||
MerkleBackpressureEnum::None => info!("Merkle Backpressure is not activated"),
|
||||
MerkleBackpressureEnum::FixedQueue(v) => info!(
|
||||
"Merkle backpressure with a fixed queue size (qlen={}) is activated.",
|
||||
v.max_queue_size
|
||||
),
|
||||
}
|
||||
|
||||
Arc::new(Self {
|
||||
data,
|
||||
empty_node_hash,
|
||||
@@ -135,11 +125,6 @@ impl<F: TableSchema, R: TableReplication> MerkleUpdater<F, R> {
|
||||
k
|
||||
);
|
||||
}
|
||||
|
||||
let maybe_bound = self.data.merkle_todo_bounded_queue.clone();
|
||||
if let Some(b) = &maybe_bound {
|
||||
b.add_permits(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+1
-19
@@ -1,16 +1,12 @@
|
||||
use opentelemetry::{global, metrics::*, KeyValue};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use garage_db as db;
|
||||
|
||||
use crate::data::MerkleTodo;
|
||||
|
||||
/// TableMetrics reference all counter used for metrics
|
||||
pub struct TableMetrics {
|
||||
pub(crate) _table_size: ValueObserver<u64>,
|
||||
pub(crate) _merkle_tree_size: ValueObserver<u64>,
|
||||
pub(crate) _merkle_todo_len: ValueObserver<u64>,
|
||||
pub(crate) _merkle_todo_bounded_queue_free: ValueObserver<u64>,
|
||||
pub(crate) _gc_todo_len: ValueObserver<u64>,
|
||||
|
||||
pub(crate) get_request_counter: BoundCounter<u64>,
|
||||
@@ -29,7 +25,7 @@ impl TableMetrics {
|
||||
table_name: &'static str,
|
||||
store: db::Tree,
|
||||
merkle_tree: db::Tree,
|
||||
merkle_todo: MerkleTodo,
|
||||
merkle_todo: db::Tree,
|
||||
gc_todo: db::Tree,
|
||||
) -> Self {
|
||||
let meter = global::meter(table_name);
|
||||
@@ -76,20 +72,6 @@ impl TableMetrics {
|
||||
)
|
||||
.with_description("Merkle tree updater TODO queue length")
|
||||
.init(),
|
||||
_merkle_todo_bounded_queue_free: meter
|
||||
.u64_value_observer(
|
||||
"table.merkle_todo_bounded_queue_free",
|
||||
move |observer| {
|
||||
let maybe_bounded = merkle_todo_bounded_queue.clone();
|
||||
let free: u64 = match &maybe_bounded {
|
||||
Some(v) => v.available_permits().try_into().unwrap(),
|
||||
None => 0,
|
||||
};
|
||||
observer.observe(free, &[KeyValue::new("table_name", table_name)])
|
||||
}
|
||||
)
|
||||
.with_description("Merkle TODO queue free slots")
|
||||
.init(),
|
||||
_gc_todo_len: meter
|
||||
.u64_value_observer(
|
||||
"table.gc_todo_queue_length",
|
||||
|
||||
+1
-7
@@ -244,14 +244,8 @@ impl<F: TableSchema, R: TableReplication> TableSyncer<F, R> {
|
||||
|
||||
// All remote nodes have written those items, now we can delete them locally
|
||||
let mut not_removed = 0;
|
||||
let maybe_bounded = self.data.merkle_todo_bounded_queue.clone();
|
||||
if let Some(b) = maybe_bounded {
|
||||
b.acquire_many(items.len() as u32).await.unwrap().forget();
|
||||
}
|
||||
|
||||
for (k, v) in items.iter() {
|
||||
let removed = self.data.delete_if_equal(&k[..], &v[..])?;
|
||||
if !removed {
|
||||
if !self.data.delete_if_equal(&k[..], &v[..])? {
|
||||
not_removed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-15
@@ -14,7 +14,6 @@ use opentelemetry::{
|
||||
use garage_db as db;
|
||||
|
||||
use garage_util::background::BackgroundRunner;
|
||||
use garage_util::config::MerkleBackpressureEnum;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::Error;
|
||||
use garage_util::metrics::RecordDuration;
|
||||
@@ -69,18 +68,12 @@ impl<F: TableSchema> Rpc for TableRpc<F> {
|
||||
impl<F: TableSchema, R: TableReplication> Table<F, R> {
|
||||
// =============== PUBLIC INTERFACE FUNCTIONS (new, insert, get, etc) ===============
|
||||
|
||||
pub fn new(
|
||||
instance: F,
|
||||
replication: R,
|
||||
system: Arc<System>,
|
||||
db: &db::Db,
|
||||
config: &MerkleBackpressureEnum,
|
||||
) -> Arc<Self> {
|
||||
pub fn new(instance: F, replication: R, system: Arc<System>, db: &db::Db) -> Arc<Self> {
|
||||
let endpoint = system
|
||||
.netapp
|
||||
.endpoint(format!("garage_table/table.rs/Rpc:{}", F::TABLE_NAME));
|
||||
|
||||
let data = TableData::new(system.clone(), instance, replication, db, config);
|
||||
let data = TableData::new(system.clone(), instance, replication, db);
|
||||
|
||||
let merkle_updater = MerkleUpdater::new(data.clone());
|
||||
|
||||
@@ -138,8 +131,7 @@ impl<F: TableSchema, R: TableReplication> Table<F, R> {
|
||||
who.as_ref(),
|
||||
rpc,
|
||||
RequestStrategy::with_priority(PRIO_NORMAL)
|
||||
.with_quorum(self.data.replication.write_quorum())
|
||||
.with_write_limiter(),
|
||||
.with_quorum(self.data.replication.write_quorum()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -535,10 +527,6 @@ impl<F: TableSchema, R: TableReplication> EndpointHandler<TableRpc<F>> for Table
|
||||
Ok(TableRpc::Update(values))
|
||||
}
|
||||
TableRpc::Update(pairs) => {
|
||||
let maybe_bounded = self.data.merkle_todo_bounded_queue.clone();
|
||||
if let Some(b) = maybe_bounded {
|
||||
b.acquire_many(pairs.len() as u32).await.unwrap().forget();
|
||||
}
|
||||
self.data.update_many(pairs)?;
|
||||
Ok(TableRpc::Ok)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_util"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
+3
-45
@@ -136,9 +136,9 @@ pub struct Config {
|
||||
#[serde(default = "Default::default")]
|
||||
pub admin: AdminConfig,
|
||||
|
||||
/// --- Experimental
|
||||
#[serde(default = "Default::default")]
|
||||
pub experimental: ExperimentalConfig,
|
||||
/// Allow punycode in bucket names
|
||||
#[serde(default)]
|
||||
pub allow_punycode: bool,
|
||||
}
|
||||
|
||||
/// Value for data_dir: either a single directory or a list of dirs with attributes
|
||||
@@ -259,40 +259,6 @@ pub struct KubernetesDiscoveryConfig {
|
||||
pub skip_crd: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
pub struct ExperimentalConfig {
|
||||
pub merkle_backpressure: MerkleBackpressureEnum,
|
||||
pub rpc_in_flight_limiters: RpcInFlightLimiterEnum,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
#[serde(rename_all = "lowercase", tag = "kind")]
|
||||
pub enum MerkleBackpressureEnum {
|
||||
#[default]
|
||||
None,
|
||||
FixedQueue(MerkleFixedQueue),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
#[serde(rename_all = "lowercase", tag = "kind")]
|
||||
pub enum RpcInFlightLimiterEnum {
|
||||
#[default]
|
||||
None,
|
||||
FixedSize(InFlightFixedSize),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
pub struct InFlightFixedSize {
|
||||
#[serde(default = "default_max_table_write")]
|
||||
pub max_table_write: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
pub struct MerkleFixedQueue {
|
||||
#[serde(default = "default_max_queue_size")]
|
||||
pub max_queue_size: usize,
|
||||
}
|
||||
|
||||
/// Read and parse configuration
|
||||
pub fn read_config(config_file: PathBuf) -> Result<Config, Error> {
|
||||
let config = std::fs::read_to_string(config_file)?;
|
||||
@@ -319,14 +285,6 @@ fn default_compression() -> Option<i32> {
|
||||
Some(1)
|
||||
}
|
||||
|
||||
fn default_max_table_write() -> usize {
|
||||
64
|
||||
}
|
||||
|
||||
fn default_max_queue_size() -> usize {
|
||||
256
|
||||
}
|
||||
|
||||
fn deserialize_compression<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_web"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
+22
-2
@@ -397,10 +397,30 @@ fn error_to_res(e: Error) -> Response<BoxBody<Error>> {
|
||||
// was a HEAD request or we couldn't get the error document)
|
||||
// We do NOT enter this code path when returning the bucket's
|
||||
// error document (this is handled in serve_file)
|
||||
let body = string_body(format!("{}\n", e));
|
||||
let mut http_error = Response::new(body);
|
||||
let mut body_str = format!(
|
||||
r"<title>{http_code} {code_text}</title>
|
||||
<h1>{http_code} {code_text}</h1>",
|
||||
http_code = e.http_status_code().as_u16(),
|
||||
code_text = e.http_status_code().canonical_reason().unwrap_or("Unknown"),
|
||||
);
|
||||
if let Error::ApiError(ref err) = e {
|
||||
body_str.push_str(&format!(
|
||||
r"
|
||||
<ul>
|
||||
<li>Code: {s3_code}</li>
|
||||
<li>Message: {s3_message}.</li>
|
||||
</ul>",
|
||||
s3_code = err.aws_code(),
|
||||
s3_message = err,
|
||||
));
|
||||
}
|
||||
let mut http_error = Response::new(string_body(body_str));
|
||||
*http_error.status_mut() = e.http_status_code();
|
||||
e.add_headers(http_error.headers_mut());
|
||||
http_error.headers_mut().insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
"text/html; charset=utf-8".parse().unwrap(),
|
||||
);
|
||||
http_error
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user