mirror of
https://git.deuxfleurs.fr/Deuxfleurs/garage.git
synced 2026-11-11 08:39:16 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d38d62f4d7 | ||
|
|
2885806e00 | ||
|
|
52437e4298 | ||
|
|
abcef7a3fd | ||
|
|
5d338f0b8f | ||
|
|
590c9bb4db | ||
|
|
c56b7e20c3 | ||
|
|
2f21181ccb | ||
|
|
2d1c073d2f | ||
|
|
5e7307cbf3 | ||
|
|
fd0e23e984 | ||
|
|
d7506b282c | ||
|
|
6bbdca2e48 | ||
|
|
c6d6cc1fc3 | ||
|
|
5fa6df6ee3 | ||
|
|
c6bed26347 | ||
|
|
d25e631a4a | ||
|
|
514eb29874 | ||
|
|
8ba6454e21 | ||
|
|
9dcc5232a6 | ||
|
|
1e13a66b42 | ||
|
|
2c9e849bbf | ||
|
|
34baade499 | ||
|
|
2f2a96b51d | ||
|
|
c9156f6828 | ||
|
|
4629ee25f7 | ||
|
|
a826c361a9 | ||
|
|
fb6db494cc | ||
|
|
97e2fa5b8b | ||
|
|
cfd259190f | ||
|
|
48e0436f29 | ||
|
|
9c745548c4 | ||
|
|
f7d9c2b383 | ||
|
|
e6862c5d3d | ||
|
|
d032e2017c | ||
|
|
0b12debf6c | ||
|
|
795b4a41b7 | ||
|
|
fd2472d488 | ||
|
|
d2a064bb1b | ||
|
|
88b4623bf1 | ||
|
|
325f79012c | ||
|
|
eb40475f1e | ||
|
|
22c0420607 | ||
|
|
1bd7689301 | ||
|
|
ec0da3b644 | ||
|
|
9511b20153 | ||
|
|
d067a40b3f | ||
|
|
ff6ec62d54 | ||
|
|
004eb94e14 | ||
|
|
46f620119b | ||
|
|
576d0d950e | ||
|
|
85a07c87d7 | ||
|
|
1f645830a4 | ||
|
|
5f308bd688 | ||
|
|
df758e8e0d | ||
|
|
e83864af24 | ||
|
|
3b49dd9e63 | ||
|
|
cef8d75983 | ||
|
|
cd0728cd20 | ||
|
|
0951b5db75 | ||
|
|
3d94eb8d4b | ||
|
|
004866caac | ||
|
|
913e6da41b | ||
|
|
e4881e62f1 | ||
|
|
7ccbfda26d | ||
|
|
6b19d7628e | ||
|
|
411f1d495c | ||
|
|
ba68506c36 | ||
|
|
21c83ab311 | ||
|
|
2e03d90585 | ||
|
|
29ce490dd6 | ||
|
|
c3e8e5e38c | ||
|
|
62a3003cca | ||
|
|
3151695011 | ||
|
|
f034e834fa | ||
|
|
bf0f792418 | ||
|
|
61f3de6496 | ||
|
|
71655c1e89 | ||
|
|
7c8fc04b96 | ||
|
|
f914db057a | ||
|
|
406b6da163 | ||
|
|
9f468b4439 | ||
|
|
97be7b38fa | ||
|
|
6a1079c412 | ||
|
|
b1629dd355 | ||
|
|
d405a9f839 | ||
|
|
7b9c047b11 | ||
|
|
10bbb26b30 | ||
|
|
89ff9f5576 | ||
|
|
bdaf55ab3f | ||
|
|
e96014ca60 | ||
|
|
568c4954e9 | ||
|
|
fe937c2901 | ||
|
|
3192088aac | ||
|
|
5a89350b38 | ||
|
|
3caea5fc06 | ||
|
|
ebc0e9319e | ||
|
|
f8c6a8373d | ||
|
|
076ce04fe5 | ||
|
|
f37d5d2b08 | ||
|
|
819f4f0050 | ||
|
|
69ddaafc60 | ||
|
|
145130481e | ||
|
|
6ed78abb5c | ||
|
|
19454c1679 | ||
|
|
1c03941b19 | ||
|
|
4f0b923c4f | ||
|
|
420bbc162d | ||
|
|
12ea4cda5f | ||
|
|
5fefbd94e9 | ||
|
|
ba810b2e81 | ||
|
|
f8ed3fdbc4 | ||
|
|
2daeb89834 | ||
|
|
4cb45bd398 | ||
|
|
d5ad797ad7 | ||
|
|
a99925e0ed | ||
|
|
f538dc34d3 | ||
|
|
ed58f8b0fe | ||
|
|
5037b97dd4 | ||
|
|
af1a530834 | ||
|
|
c99bfe69ea | ||
|
|
831f2b0207 | ||
|
|
c1eb1610ba | ||
|
|
5560a963e0 | ||
|
|
2aaba39ddc | ||
|
|
47467df83e | ||
|
|
9b7fea4cb0 | ||
|
|
44ce6ae5b4 | ||
|
|
22487ceddf | ||
|
|
6ccfbb2986 | ||
|
|
c939d2a936 | ||
|
|
65e9dde8c9 | ||
|
|
c9b733a4a6 |
124 changed files with 14963 additions and 4874 deletions
|
|
@ -35,7 +35,15 @@ steps:
|
|||
- matrix:
|
||||
ARCH: i386
|
||||
|
||||
- name: upgrade tests
|
||||
- name: upgrade tests from v1.0.0
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix-shell --attr ci --run "./script/test-upgrade.sh v1.0.0 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
|
||||
when:
|
||||
- matrix:
|
||||
ARCH: amd64
|
||||
|
||||
- name: upgrade tests from v0.8.4
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
commands:
|
||||
- nix-shell --attr ci --run "./script/test-upgrade.sh v0.8.4 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
|
||||
|
|
|
|||
63
Cargo.lock
generated
63
Cargo.lock
generated
|
|
@ -1200,7 +1200,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"async-trait",
|
||||
|
|
@ -1240,7 +1240,6 @@ dependencies = [
|
|||
"opentelemetry-otlp",
|
||||
"opentelemetry-prometheus",
|
||||
"parse_duration",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2",
|
||||
|
|
@ -1251,17 +1250,22 @@ dependencies = [
|
|||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garage_api_admin"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"async-trait",
|
||||
"bytesize",
|
||||
"chrono",
|
||||
"err-derive",
|
||||
"format_table",
|
||||
"futures",
|
||||
"garage_api_common",
|
||||
"garage_block",
|
||||
"garage_model",
|
||||
"garage_rpc",
|
||||
"garage_table",
|
||||
|
|
@ -1271,23 +1275,26 @@ dependencies = [
|
|||
"hyper 1.6.0",
|
||||
"opentelemetry",
|
||||
"opentelemetry-prometheus",
|
||||
"paste",
|
||||
"prometheus",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garage_api_common"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc32c",
|
||||
"crc32fast",
|
||||
"crc64fast-nvme",
|
||||
"crypto-common",
|
||||
"err-derive",
|
||||
"futures",
|
||||
|
|
@ -1316,7 +1323,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage_api_k2v"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"err-derive",
|
||||
|
|
@ -1339,7 +1346,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage_api_s3"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"async-compression",
|
||||
|
|
@ -1348,6 +1355,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"crc32c",
|
||||
"crc32fast",
|
||||
"crc64fast-nvme",
|
||||
"err-derive",
|
||||
"form_urlencoded",
|
||||
"futures",
|
||||
|
|
@ -1359,6 +1367,7 @@ dependencies = [
|
|||
"garage_table",
|
||||
"garage_util",
|
||||
"hex",
|
||||
"hmac",
|
||||
"http 1.3.1",
|
||||
"http-body-util",
|
||||
"http-range",
|
||||
|
|
@ -1384,7 +1393,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage_block"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-compression",
|
||||
|
|
@ -1395,7 +1404,6 @@ dependencies = [
|
|||
"garage_db",
|
||||
"garage_net",
|
||||
"garage_rpc",
|
||||
"garage_table",
|
||||
"garage_util",
|
||||
"hex",
|
||||
"opentelemetry",
|
||||
|
|
@ -1409,7 +1417,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage_db"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"err-derive",
|
||||
"heed",
|
||||
|
|
@ -1422,8 +1430,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage_model"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"async-trait",
|
||||
"base64 0.21.7",
|
||||
"blake2",
|
||||
|
|
@ -1449,7 +1458,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage_net"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"bytes",
|
||||
|
|
@ -1474,7 +1483,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage_rpc"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
|
|
@ -1506,7 +1515,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage_table"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
|
|
@ -1527,7 +1536,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage_util"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
|
|
@ -1559,7 +1568,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "garage_web"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"err-derive",
|
||||
"garage_api_common",
|
||||
|
|
@ -2219,6 +2228,7 @@ checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
|
|||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4677,6 +4687,29 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0"
|
||||
dependencies = [
|
||||
"indexmap 2.8.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa-gen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.1"
|
||||
|
|
|
|||
29
Cargo.toml
29
Cargo.toml
|
|
@ -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 = "2.0.0", path = "src/api/common" }
|
||||
garage_api_admin = { version = "2.0.0", path = "src/api/admin" }
|
||||
garage_api_s3 = { version = "2.0.0", path = "src/api/s3" }
|
||||
garage_api_k2v = { version = "2.0.0", path = "src/api/k2v" }
|
||||
garage_block = { version = "2.0.0", path = "src/block" }
|
||||
garage_db = { version = "2.0.0", path = "src/db", default-features = false }
|
||||
garage_model = { version = "2.0.0", path = "src/model", default-features = false }
|
||||
garage_net = { version = "2.0.0", path = "src/net" }
|
||||
garage_rpc = { version = "2.0.0", path = "src/rpc" }
|
||||
garage_table = { version = "2.0.0", path = "src/table" }
|
||||
garage_util = { version = "2.0.0", path = "src/util" }
|
||||
garage_web = { version = "2.0.0", path = "src/web" }
|
||||
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
|
||||
|
||||
# External crates from crates.io
|
||||
|
|
@ -48,9 +48,10 @@ blake2 = "0.10"
|
|||
bytes = "1.0"
|
||||
bytesize = "1.1"
|
||||
cfg-if = "1.0"
|
||||
chrono = "0.4"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
crc32fast = "1.4"
|
||||
crc32c = "0.6"
|
||||
crc64fast-nvme = "1.2"
|
||||
crypto-common = "0.1"
|
||||
err-derive = "0.3"
|
||||
gethostname = "0.4"
|
||||
|
|
@ -67,6 +68,7 @@ mktemp = "0.5"
|
|||
nix = { version = "0.29", default-features = false, features = ["fs"] }
|
||||
nom = "7.1"
|
||||
parse_duration = "2.1"
|
||||
paste = "1.0"
|
||||
pin-project = "1.0.12"
|
||||
pnet_datalink = "0.34"
|
||||
rand = "0.8"
|
||||
|
|
@ -100,6 +102,7 @@ serde = { version = "1.0", default-features = false, features = ["derive", "rc"]
|
|||
serde_bytes = "0.11"
|
||||
serde_json = "1.0"
|
||||
toml = { version = "0.8", default-features = false, features = ["parse"] }
|
||||
utoipa = { version = "5.3.1", features = ["chrono"] }
|
||||
|
||||
# newer version requires rust edition 2021
|
||||
k8s-openapi = { version = "0.21", features = ["v1_24"] }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Garage Adminstration API v0</title>
|
||||
<title>Garage adminstration API v0</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Garage Adminstration API v0</title>
|
||||
<title>Garage adminstration API v1</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
|
|
|||
24
doc/api/garage-admin-v2.html
Normal file
24
doc/api/garage-admin-v2.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Garage adminstration API v2</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="./css/redoc.css" rel="stylesheet">
|
||||
|
||||
<!--
|
||||
Redoc doesn't change outer page styles
|
||||
-->
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url='./garage-admin-v2.json'></redoc>
|
||||
<script src="./redoc.standalone.js"> </script>
|
||||
</body>
|
||||
</html>
|
||||
4331
doc/api/garage-admin-v2.json
Normal file
4331
doc/api/garage-admin-v2.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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. `v2.0.0`) and not the `latest` tag.
|
||||
For this example, we will use the latest published version at the time of the writing which is `v2.0.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:v2.0.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:v2.0.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:v2.0.0
|
||||
network_mode: "host"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ docker run \
|
|||
-v /etc/garage.toml:/path/to/garage.toml \
|
||||
-v /var/lib/garage/meta:/path/to/garage/meta \
|
||||
-v /var/lib/garage/data:/path/to/garage/data \
|
||||
dxflrs/garage:v1.1.0
|
||||
dxflrs/garage:v2.0.0
|
||||
```
|
||||
|
||||
Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ add_host_to_metrics = true
|
|||
[admin]
|
||||
api_bind_addr = "0.0.0.0:3903"
|
||||
metrics_token = "BCAdFjoa9G0KJR0WXnHHm7fs1ZAbfpI8iIZ+Z/a2NgI="
|
||||
metrics_require_token = true
|
||||
admin_token = "UkLeGWEvHnXBqnueR3ISEMWpOnm40jH2tM2HnnL/0F4="
|
||||
trace_sink = "http://localhost:4317"
|
||||
```
|
||||
|
|
@ -145,6 +146,7 @@ The `[s3_web]` section:
|
|||
|
||||
The `[admin]` section:
|
||||
[`api_bind_addr`](#admin_api_bind_addr),
|
||||
[`metrics_require_token`](#admin_metrics_require_token),
|
||||
[`metrics_token`/`metrics_token_file`](#admin_metrics_token),
|
||||
[`admin_token`/`admin_token_file`](#admin_token),
|
||||
[`trace_sink`](#admin_trace_sink),
|
||||
|
|
@ -767,10 +769,34 @@ See [administration API reference](@/documentation/reference-manual/admin-api.md
|
|||
Alternatively, since `v0.8.5`, a path can be used to create a unix socket. Note that for security reasons,
|
||||
the socket will have 0220 mode. Make sure to set user and group permissions accordingly.
|
||||
|
||||
#### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
|
||||
|
||||
The token for accessing all administration functions on the admin endpoint,
|
||||
with the exception of the metrics endpoint (see `metrics_token`).
|
||||
|
||||
You can use any random string for this value. We recommend generating a random
|
||||
token with `openssl rand -base64 32`.
|
||||
|
||||
For Garage version earlier than `v2.0`, if this token is not set,
|
||||
access to these endpoints is disabled entirely.
|
||||
|
||||
Since Garage `v2.0`, additional admin API tokens can be defined dynamically
|
||||
in your Garage cluster using administration commands. This new admin token system
|
||||
is more flexible since it allows admin tokens to have an expiration date,
|
||||
and to have a scope restricted to certain admin API functions. If `admin_token`
|
||||
is set, it behaves as an admin token without expiration and with full scope.
|
||||
Otherwise, only admin API tokens defined dynamically can be used.
|
||||
|
||||
`admin_token` was introduced in Garage `v0.7.2`.
|
||||
`admin_token_file` and the `GARAGE_ADMIN_TOKEN` environment variable are supported since Garage `v0.8.2`.
|
||||
|
||||
`GARAGE_ADMIN_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
||||
|
||||
#### `metrics_token`, `metrics_token_file` or `GARAGE_METRICS_TOKEN`, `GARAGE_METRICS_TOKEN_FILE` (env) {#admin_metrics_token}
|
||||
|
||||
The token for accessing the Metrics endpoint. If this token is not set, the
|
||||
Metrics endpoint can be accessed without access control.
|
||||
The token for accessing the Prometheus metrics endpoint (`/metrics`).
|
||||
If this token is not set, and unless `metrics_require_token` is set to `true`,
|
||||
the metrics endpoint can be accessed without access control.
|
||||
|
||||
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
|
||||
|
||||
|
|
@ -779,17 +805,12 @@ You can use any random string for this value. We recommend generating a random t
|
|||
|
||||
`GARAGE_METRICS_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
||||
|
||||
#### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
|
||||
#### `metrics_require_token` (since `v2.0.0`) {#admin_metrics_require_token}
|
||||
|
||||
The token for accessing all of the other administration endpoints. If this
|
||||
token is not set, access to these endpoints is disabled entirely.
|
||||
|
||||
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
|
||||
|
||||
`admin_token` was introduced in Garage `v0.7.2`.
|
||||
`admin_token_file` and the `GARAGE_ADMIN_TOKEN` environment variable are supported since Garage `v0.8.2`.
|
||||
|
||||
`GARAGE_ADMIN_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
||||
If this is set to `true`, accessing the metrics endpoint will always require
|
||||
an access token. Valid tokens include the `metrics_token` if it is set,
|
||||
and admin API token defined dynamicaly in Garage which have
|
||||
the `Metrics` endpoint in their scope.
|
||||
|
||||
#### `trace_sink` {#admin_trace_sink}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ We will bump the version numbers prefixed to each API endpoint each time the syn
|
|||
or semantics change, meaning that code that relies on these endpoints will break
|
||||
when changes are introduced.
|
||||
|
||||
The Garage administration API was introduced in version 0.7.2, this document
|
||||
does not apply to older versions of Garage.
|
||||
The Garage administration API was introduced in version 0.7.2, and was
|
||||
changed several times.
|
||||
This document applies only to the Garage v2 API (starting with Garage v2.0.0).
|
||||
|
||||
|
||||
## Access control
|
||||
|
|
@ -52,34 +53,28 @@ Returns an HTTP status 200 if the node is ready to answer user's requests,
|
|||
and an HTTP status 503 (Service Unavailable) if there are some partitions
|
||||
for which a quorum of nodes is not available.
|
||||
A simple textual message is also returned in a body with content-type `text/plain`.
|
||||
See `/v1/health` for an API that also returns JSON output.
|
||||
See `/v2/GetClusterHealth` for an API that also returns JSON output.
|
||||
|
||||
### Other special endpoints
|
||||
|
||||
#### CheckDomain `GET /check?domain=<domain>`
|
||||
|
||||
Checks whether this Garage cluster serves a website for domain `<domain>`.
|
||||
Returns HTTP 200 Ok if yes, or HTTP 4xx if no website is available for this domain.
|
||||
|
||||
### Cluster operations
|
||||
|
||||
#### GetClusterStatus `GET /v1/status`
|
||||
#### GetClusterStatus `GET /v2/GetClusterStatus`
|
||||
|
||||
Returns the cluster's current status in JSON, including:
|
||||
|
||||
- ID of the node being queried and its version of the Garage daemon
|
||||
- Live nodes
|
||||
- Currently configured cluster layout
|
||||
- Staged changes to the cluster layout
|
||||
|
||||
Example response body:
|
||||
|
||||
```json
|
||||
{
|
||||
"node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df",
|
||||
"garageVersion": "v1.1.0",
|
||||
"garageFeatures": [
|
||||
"k2v",
|
||||
"lmdb",
|
||||
"sqlite",
|
||||
"metrics",
|
||||
"bundled-libs"
|
||||
],
|
||||
"rustVersion": "1.68.0",
|
||||
"dbEngine": "LMDB (using Heed crate)",
|
||||
"layoutVersion": 5,
|
||||
"nodes": [
|
||||
{
|
||||
|
|
@ -169,7 +164,7 @@ Example response body:
|
|||
}
|
||||
```
|
||||
|
||||
#### GetClusterHealth `GET /v1/health`
|
||||
#### GetClusterHealth `GET /v2/GetClusterHealth`
|
||||
|
||||
Returns the cluster's current health in JSON format, with the following variables:
|
||||
|
||||
|
|
@ -202,7 +197,7 @@ Example response body:
|
|||
}
|
||||
```
|
||||
|
||||
#### ConnectClusterNodes `POST /v1/connect`
|
||||
#### ConnectClusterNodes `POST /v2/ConnectClusterNodes`
|
||||
|
||||
Instructs this Garage node to connect to other Garage nodes at specified addresses.
|
||||
|
||||
|
|
@ -232,7 +227,7 @@ Example response:
|
|||
]
|
||||
```
|
||||
|
||||
#### GetClusterLayout `GET /v1/layout`
|
||||
#### GetClusterLayout `GET /v2/GetClusterLayout`
|
||||
|
||||
Returns the cluster's current layout in JSON, including:
|
||||
|
||||
|
|
@ -293,7 +288,7 @@ Example response body:
|
|||
}
|
||||
```
|
||||
|
||||
#### UpdateClusterLayout `POST /v1/layout`
|
||||
#### UpdateClusterLayout `POST /v2/UpdateClusterLayout`
|
||||
|
||||
Send modifications to the cluster layout. These modifications will
|
||||
be included in the staged role changes, visible in subsequent calls
|
||||
|
|
@ -330,7 +325,7 @@ This returns the new cluster layout with the proposed staged changes,
|
|||
as returned by GetClusterLayout.
|
||||
|
||||
|
||||
#### ApplyClusterLayout `POST /v1/layout/apply`
|
||||
#### ApplyClusterLayout `POST /v2/ApplyClusterLayout`
|
||||
|
||||
Applies to the cluster the layout changes currently registered as
|
||||
staged layout changes.
|
||||
|
|
@ -350,23 +345,11 @@ existing layout in the cluster.
|
|||
This returns the message describing all the calculations done to compute the new
|
||||
layout, as well as the description of the layout as returned by GetClusterLayout.
|
||||
|
||||
#### RevertClusterLayout `POST /v1/layout/revert`
|
||||
#### RevertClusterLayout `POST /v2/RevertClusterLayout`
|
||||
|
||||
Clears all of the staged layout changes.
|
||||
|
||||
Request body format:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 13
|
||||
}
|
||||
```
|
||||
|
||||
Reverting the staged changes is done by incrementing the version number
|
||||
and clearing the contents of the staged change list.
|
||||
Similarly to the CLI, the body must include the incremented
|
||||
version number, which MUST be 1 + the value of the currently
|
||||
existing layout in the cluster.
|
||||
This requests contains an empty body.
|
||||
|
||||
This returns the new cluster layout with all changes reverted,
|
||||
as returned by GetClusterLayout.
|
||||
|
|
@ -374,7 +357,7 @@ as returned by GetClusterLayout.
|
|||
|
||||
### Access key operations
|
||||
|
||||
#### ListKeys `GET /v1/key`
|
||||
#### ListKeys `GET /v2/ListKeys`
|
||||
|
||||
Returns all API access keys in the cluster.
|
||||
|
||||
|
|
@ -393,8 +376,8 @@ Example response:
|
|||
]
|
||||
```
|
||||
|
||||
#### GetKeyInfo `GET /v1/key?id=<acces key id>`
|
||||
#### GetKeyInfo `GET /v1/key?search=<pattern>`
|
||||
#### GetKeyInfo `GET /v2/GetKeyInfo?id=<acces key id>`
|
||||
#### GetKeyInfo `GET /v2/GetKeyInfo?search=<pattern>`
|
||||
|
||||
Returns information about the requested API access key.
|
||||
|
||||
|
|
@ -468,7 +451,7 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
#### CreateKey `POST /v1/key`
|
||||
#### CreateKey `POST /v2/CreateKey`
|
||||
|
||||
Creates a new API access key.
|
||||
|
||||
|
|
@ -483,7 +466,7 @@ Request body format:
|
|||
This returns the key info, including the created secret key,
|
||||
in the same format as the result of GetKeyInfo.
|
||||
|
||||
#### ImportKey `POST /v1/key/import`
|
||||
#### ImportKey `POST /v2/ImportKey`
|
||||
|
||||
Imports an existing API key.
|
||||
This will check that the imported key is in the valid format, i.e.
|
||||
|
|
@ -501,7 +484,7 @@ Request body format:
|
|||
|
||||
This returns the key info in the same format as the result of GetKeyInfo.
|
||||
|
||||
#### UpdateKey `POST /v1/key?id=<acces key id>`
|
||||
#### UpdateKey `POST /v2/UpdateKey?id=<acces key id>`
|
||||
|
||||
Updates information about the specified API access key.
|
||||
|
||||
|
|
@ -523,14 +506,14 @@ The possible flags in `allow` and `deny` are: `createBucket`.
|
|||
|
||||
This returns the key info in the same format as the result of GetKeyInfo.
|
||||
|
||||
#### DeleteKey `DELETE /v1/key?id=<acces key id>`
|
||||
#### DeleteKey `POST /v2/DeleteKey?id=<acces key id>`
|
||||
|
||||
Deletes an API access key.
|
||||
|
||||
|
||||
### Bucket operations
|
||||
|
||||
#### ListBuckets `GET /v1/bucket`
|
||||
#### ListBuckets `GET /v2/ListBuckets`
|
||||
|
||||
Returns all storage buckets in the cluster.
|
||||
|
||||
|
|
@ -572,8 +555,8 @@ Example response:
|
|||
]
|
||||
```
|
||||
|
||||
#### GetBucketInfo `GET /v1/bucket?id=<bucket id>`
|
||||
#### GetBucketInfo `GET /v1/bucket?globalAlias=<alias>`
|
||||
#### GetBucketInfo `GET /v2/GetBucketInfo?id=<bucket id>`
|
||||
#### GetBucketInfo `GET /v2/GetBucketInfo?globalAlias=<alias>`
|
||||
|
||||
Returns information about the requested storage bucket.
|
||||
|
||||
|
|
@ -616,7 +599,7 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
#### CreateBucket `POST /v1/bucket`
|
||||
#### CreateBucket `POST /v2/CreateBucket`
|
||||
|
||||
Creates a new storage bucket.
|
||||
|
||||
|
|
@ -656,7 +639,7 @@ or no alias at all.
|
|||
Technically, you can also specify both `globalAlias` and `localAlias` and that would create
|
||||
two aliases, but I don't see why you would want to do that.
|
||||
|
||||
#### UpdateBucket `PUT /v1/bucket?id=<bucket id>`
|
||||
#### UpdateBucket `POST /v2/UpdateBucket?id=<bucket id>`
|
||||
|
||||
Updates configuration of the given bucket.
|
||||
|
||||
|
|
@ -688,16 +671,38 @@ In `quotas`: new values of `maxSize` and `maxObjects` must both be specified, or
|
|||
to remove the quotas. An absent value will be considered the same as a `null`. It is not possible
|
||||
to change only one of the two quotas.
|
||||
|
||||
#### DeleteBucket `DELETE /v1/bucket?id=<bucket id>`
|
||||
#### DeleteBucket `POST /v2/DeleteBucket?id=<bucket id>`
|
||||
|
||||
Deletes a storage bucket. A bucket cannot be deleted if it is not empty.
|
||||
|
||||
Warning: this will delete all aliases associated with the bucket!
|
||||
|
||||
#### CleanupIncompleteUploads `POST /v2/CleanupIncompleteUploads`
|
||||
|
||||
Cleanup all incomplete uploads in a bucket that are older than a specified number
|
||||
of seconds.
|
||||
|
||||
Request body format:
|
||||
|
||||
```json
|
||||
{
|
||||
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||
"olderThanSecs": 3600
|
||||
}
|
||||
```
|
||||
|
||||
Response format
|
||||
|
||||
```json
|
||||
{
|
||||
"uploadsDeleted": 12
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Operations on permissions for keys on buckets
|
||||
|
||||
#### BucketAllowKey `POST /v1/bucket/allow`
|
||||
#### AllowBucketKey `POST /v2/AllowBucketKey`
|
||||
|
||||
Allows a key to do read/write/owner operations on a bucket.
|
||||
|
||||
|
|
@ -718,7 +723,7 @@ Request body format:
|
|||
Flags in `permissions` which have the value `true` will be activated.
|
||||
Other flags will remain unchanged.
|
||||
|
||||
#### BucketDenyKey `POST /v1/bucket/deny`
|
||||
#### DenyBucketKey `POST /v2/DenyBucketKey`
|
||||
|
||||
Denies a key from doing read/write/owner operations on a bucket.
|
||||
|
||||
|
|
@ -742,19 +747,35 @@ Other flags will remain unchanged.
|
|||
|
||||
### Operations on bucket aliases
|
||||
|
||||
#### GlobalAliasBucket `PUT /v1/bucket/alias/global?id=<bucket id>&alias=<global alias>`
|
||||
#### AddBucketAlias `POST /v2/AddBucketAlias`
|
||||
|
||||
Empty body. Creates a global alias for a bucket.
|
||||
Creates an alias for a bucket in the namespace of a specific access key.
|
||||
To create a global alias, specify the `globalAlias` field.
|
||||
To create a local alias, specify the `localAlias` and `accessKeyId` fields.
|
||||
|
||||
#### GlobalUnaliasBucket `DELETE /v1/bucket/alias/global?id=<bucket id>&alias=<global alias>`
|
||||
Request body format:
|
||||
|
||||
Removes a global alias for a bucket.
|
||||
```json
|
||||
{
|
||||
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||
"globalAlias": "my-bucket"
|
||||
}
|
||||
```
|
||||
|
||||
#### LocalAliasBucket `PUT /v1/bucket/alias/local?id=<bucket id>&accessKeyId=<access key ID>&alias=<local alias>`
|
||||
or:
|
||||
|
||||
Empty body. Creates a local alias for a bucket in the namespace of a specific access key.
|
||||
```json
|
||||
{
|
||||
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||
"localAlias": "my-bucket"
|
||||
}
|
||||
```
|
||||
|
||||
#### LocalUnaliasBucket `DELETE /v1/bucket/alias/local?id=<bucket id>&accessKeyId<access key ID>&alias=<local alias>`
|
||||
#### RemoveBucketAlias `POST /v2/RemoveBucketAlias`
|
||||
|
||||
Removes a local alias for a bucket in the namespace of a specific access key.
|
||||
Removes an alias for a bucket in the namespace of a specific access key.
|
||||
To remove a global alias, specify the `globalAlias` field.
|
||||
To remove a local alias, specify the `localAlias` and `accessKeyId` fields.
|
||||
|
||||
Request body format: same as AddBucketAlias.
|
||||
|
|
|
|||
|
|
@ -17,13 +17,19 @@ else
|
|||
fi
|
||||
|
||||
$GARAGE_BIN -c /tmp/config.1.toml bucket create eprouvette
|
||||
if [ "$GARAGE_08" = "1" ]; then
|
||||
if [ "$GARAGE_OLDVER" = "v08" ]; then
|
||||
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml key new --name opérateur)
|
||||
else
|
||||
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
|
||||
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
|
||||
elif [ "$GARAGE_OLDVER" = "v1" ]; then
|
||||
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml key create opérateur)
|
||||
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
|
||||
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
|
||||
else
|
||||
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml json-api CreateKey '{"name":"opérateur"}')
|
||||
ACCESS_KEY=`echo $KEY_INFO|jq -r .accessKeyId`
|
||||
SECRET_KEY=`echo $KEY_INFO|jq -r .secretAccessKey`
|
||||
fi
|
||||
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
|
||||
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
|
||||
$GARAGE_BIN -c /tmp/config.1.toml bucket allow eprouvette --read --write --owner --key $ACCESS_KEY
|
||||
echo "$ACCESS_KEY $SECRET_KEY" > /tmp/garage.s3
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ until $GARAGE_BIN -c /tmp/config.1.toml status 2>&1|grep -q HEALTHY ; do
|
|||
sleep 1
|
||||
done
|
||||
|
||||
if [ "$GARAGE_08" = "1" ]; then
|
||||
if [ "$GARAGE_OLDVER" = "v08" ]; then
|
||||
$GARAGE_BIN -c /tmp/config.1.toml status \
|
||||
| grep 'NO ROLE' \
|
||||
| grep -Po '^[0-9a-f]+' \
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ type: application
|
|||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.7.0
|
||||
version: 0.8.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v1.1.0"
|
||||
appVersion: "v2.0.0"
|
||||
|
|
|
|||
|
|
@ -24,9 +24,17 @@ echo "============= insert data into old version cluster ================="
|
|||
export GARAGE_BIN=/tmp/old_garage
|
||||
if echo $OLD_VERSION | grep 'v0\.8\.'; then
|
||||
echo "Detected Garage v0.8.x"
|
||||
export GARAGE_08=1
|
||||
export GARAGE_OLDVER=v08
|
||||
elif (echo $OLD_VERSION | grep 'v0\.9\.') || (echo $OLD_VERSION | grep 'v1\.'); then
|
||||
echo "Detected Garage v0.9.x / v1.x"
|
||||
export GARAGE_OLDVER=v1
|
||||
fi
|
||||
|
||||
if echo $OLD_VERSION | grep 'v1\.'; then
|
||||
DO_SSEC_TEST=1
|
||||
fi
|
||||
SSEC_KEY="u8zCfnEyt5Imo/krN+sxA1DQXxLWtPJavU6T6gOVj1Y="
|
||||
|
||||
echo "⏳ Setup cluster using old version"
|
||||
$GARAGE_BIN --version
|
||||
${SCRIPT_FOLDER}/dev-clean.sh
|
||||
|
|
@ -37,7 +45,23 @@ ${SCRIPT_FOLDER}/dev-bucket.sh
|
|||
|
||||
echo "🛠️ Inserting data in old cluster"
|
||||
source ${SCRIPT_FOLDER}/dev-env-rclone.sh
|
||||
rclone copy "${SCRIPT_FOLDER}/../.git/" garage:eprouvette/test_dotgit --stats=1s --stats-log-level=NOTICE --stats-one-line
|
||||
rclone copy "${SCRIPT_FOLDER}/../.git/" garage:eprouvette/test_dotgit \
|
||||
--stats=1s --stats-log-level=NOTICE --stats-one-line
|
||||
|
||||
if [ "$DO_SSEC_TEST" = "1" ]; then
|
||||
# upload small file (should be single part)
|
||||
rclone copy "${SCRIPT_FOLDER}/test-upgrade.sh" garage:eprouvette/test-ssec \
|
||||
--s3-sse-customer-algorithm AES256 \
|
||||
--s3-sse-customer-key-base64 "$SSEC_KEY" \
|
||||
--stats=1s --stats-log-level=NOTICE --stats-one-line
|
||||
# do a multipart upload
|
||||
dd if=/dev/urandom of=/tmp/randfile-for-upgrade bs=5M count=5
|
||||
rclone copy "/tmp/randfile-for-upgrade" garage:eprouvette/test-ssec \
|
||||
--s3-chunk-size 5M \
|
||||
--s3-sse-customer-algorithm AES256 \
|
||||
--s3-sse-customer-key-base64 "$SSEC_KEY" \
|
||||
--stats=1s --stats-log-level=NOTICE --stats-one-line
|
||||
fi
|
||||
|
||||
echo "🏁 Stopping old cluster"
|
||||
killall -INT old_garage
|
||||
|
|
@ -47,7 +71,7 @@ killall -9 old_garage || true
|
|||
echo "🏁 Removing old garage version"
|
||||
rm -rv $GARAGE_BIN
|
||||
export -n GARAGE_BIN
|
||||
export -n GARAGE_08
|
||||
export -n GARAGE_OLDVER
|
||||
|
||||
echo "================ read data from new cluster ==================="
|
||||
|
||||
|
|
@ -60,7 +84,8 @@ ${SCRIPT_FOLDER}/dev-cluster.sh >> /tmp/garage.log 2>&1 &
|
|||
sleep 3
|
||||
|
||||
echo "🛠️ Retrieving data from old cluster"
|
||||
rclone copy garage:eprouvette/test_dotgit /tmp/test_dotgit --stats=1s --stats-log-level=NOTICE --stats-one-line --fast-list
|
||||
rclone copy garage:eprouvette/test_dotgit /tmp/test_dotgit \
|
||||
--stats=1s --stats-log-level=NOTICE --stats-one-line --fast-list
|
||||
|
||||
if ! diff <(find "${SCRIPT_FOLDER}/../.git" -type f | xargs md5sum | cut -d ' ' -f 1 | sort) <(find /tmp/test_dotgit -type f | xargs md5sum | cut -d ' ' -f 1 | sort); then
|
||||
echo "TEST FAILURE: directories are different"
|
||||
|
|
@ -68,6 +93,23 @@ if ! diff <(find "${SCRIPT_FOLDER}/../.git" -type f | xargs md5sum | cut -d ' '
|
|||
fi
|
||||
rm -r /tmp/test_dotgit
|
||||
|
||||
if [ "$DO_SSEC_TEST" = "1" ]; then
|
||||
rclone copy garage:eprouvette/test-ssec /tmp/test_ssec_out \
|
||||
--s3-sse-customer-algorithm AES256 \
|
||||
--s3-sse-customer-key-base64 "$SSEC_KEY" \
|
||||
--stats=1s --stats-log-level=NOTICE --stats-one-line
|
||||
if ! diff "/tmp/test_ssec_out/test-upgrade.sh" "${SCRIPT_FOLDER}/test-upgrade.sh"; then
|
||||
echo "SSEC-FAILURE (small file)"
|
||||
exit 1
|
||||
fi
|
||||
if ! diff "/tmp/test_ssec_out/randfile-for-upgrade" "/tmp/randfile-for-upgrade"; then
|
||||
echo "SSEC-FAILURE (big file)"
|
||||
exit 1
|
||||
fi
|
||||
rm -r /tmp/test_ssec_out
|
||||
rm /tmp/randfile-for-upgrade
|
||||
fi
|
||||
|
||||
echo "🏁 Teardown"
|
||||
rm -rf /tmp/garage-{data,meta}-*
|
||||
rm -rf /tmp/config.*.toml
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "garage_api_admin"
|
||||
version = "1.1.0"
|
||||
version = "2.0.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
|
|
@ -14,7 +14,9 @@ path = "lib.rs"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
format_table.workspace = true
|
||||
garage_model.workspace = true
|
||||
garage_block.workspace = true
|
||||
garage_table.workspace = true
|
||||
garage_util.workspace = true
|
||||
garage_rpc.workspace = true
|
||||
|
|
@ -22,8 +24,11 @@ garage_api_common.workspace = true
|
|||
|
||||
argon2.workspace = true
|
||||
async-trait.workspace = true
|
||||
bytesize.workspace = true
|
||||
chrono.workspace = true
|
||||
err-derive.workspace = true
|
||||
hex.workspace = true
|
||||
paste.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
futures.workspace = true
|
||||
|
|
@ -34,6 +39,7 @@ url.workspace = true
|
|||
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
|
||||
opentelemetry.workspace = true
|
||||
opentelemetry-prometheus = { workspace = true, optional = true }
|
||||
|
|
@ -41,3 +47,4 @@ prometheus = { workspace = true, optional = true }
|
|||
|
||||
[features]
|
||||
metrics = [ "opentelemetry-prometheus", "prometheus" ]
|
||||
k2v = [ "garage_model/k2v" ]
|
||||
|
|
|
|||
235
src/api/admin/admin_token.rs
Normal file
235
src/api/admin/admin_token.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use garage_table::*;
|
||||
use garage_util::time::now_msec;
|
||||
|
||||
use garage_model::admin_token_table::*;
|
||||
use garage_model::garage::Garage;
|
||||
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
use crate::{Admin, RequestHandler};
|
||||
|
||||
impl RequestHandler for ListAdminTokensRequest {
|
||||
type Response = ListAdminTokensResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<ListAdminTokensResponse, Error> {
|
||||
let now = now_msec();
|
||||
|
||||
let mut res = garage
|
||||
.admin_token_table
|
||||
.get_range(
|
||||
&EmptyKey,
|
||||
None,
|
||||
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
|
||||
10000,
|
||||
EnumerationOrder::Forward,
|
||||
)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|t| admin_token_info_results(t, now))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if garage.config.admin.admin_token.is_some() {
|
||||
res.insert(
|
||||
0,
|
||||
GetAdminTokenInfoResponse {
|
||||
id: None,
|
||||
created: None,
|
||||
name: "admin_token (from daemon configuration)".into(),
|
||||
expiration: None,
|
||||
expired: false,
|
||||
scope: vec!["*".into()],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if garage.config.admin.metrics_token.is_some() {
|
||||
res.insert(
|
||||
1,
|
||||
GetAdminTokenInfoResponse {
|
||||
id: None,
|
||||
created: None,
|
||||
name: "metrics_token (from daemon configuration)".into(),
|
||||
expiration: None,
|
||||
expired: false,
|
||||
scope: vec!["Metrics".into()],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(ListAdminTokensResponse(res))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for GetAdminTokenInfoRequest {
|
||||
type Response = GetAdminTokenInfoResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<GetAdminTokenInfoResponse, Error> {
|
||||
let token = match (self.id, self.search) {
|
||||
(Some(id), None) => get_existing_admin_token(garage, &id).await?,
|
||||
(None, Some(search)) => {
|
||||
let candidates = garage
|
||||
.admin_token_table
|
||||
.get_range(
|
||||
&EmptyKey,
|
||||
None,
|
||||
Some(KeyFilter::MatchesAndNotDeleted(search.to_string())),
|
||||
10,
|
||||
EnumerationOrder::Forward,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
if candidates.len() != 1 {
|
||||
return Err(Error::bad_request(format!(
|
||||
"{} matching admin tokens",
|
||||
candidates.len()
|
||||
)));
|
||||
}
|
||||
candidates.into_iter().next().unwrap()
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::bad_request(
|
||||
"Either id or search must be provided (but not both)",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(admin_token_info_results(&token, now_msec()))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for CreateAdminTokenRequest {
|
||||
type Response = CreateAdminTokenResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<CreateAdminTokenResponse, Error> {
|
||||
let (mut token, secret) = if self.0.name.is_some() {
|
||||
AdminApiToken::new("")
|
||||
} else {
|
||||
AdminApiToken::new(&format!("token_{}", Utc::now().format("%Y%m%d_%H%M")))
|
||||
};
|
||||
|
||||
apply_token_updates(&mut token, self.0)?;
|
||||
|
||||
garage.admin_token_table.insert(&token).await?;
|
||||
|
||||
Ok(CreateAdminTokenResponse {
|
||||
secret_token: secret,
|
||||
info: admin_token_info_results(&token, now_msec()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for UpdateAdminTokenRequest {
|
||||
type Response = UpdateAdminTokenResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<UpdateAdminTokenResponse, Error> {
|
||||
let mut token = get_existing_admin_token(&garage, &self.id).await?;
|
||||
|
||||
apply_token_updates(&mut token, self.body)?;
|
||||
|
||||
garage.admin_token_table.insert(&token).await?;
|
||||
|
||||
Ok(UpdateAdminTokenResponse(admin_token_info_results(
|
||||
&token,
|
||||
now_msec(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for DeleteAdminTokenRequest {
|
||||
type Response = DeleteAdminTokenResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<DeleteAdminTokenResponse, Error> {
|
||||
let token = get_existing_admin_token(&garage, &self.id).await?;
|
||||
|
||||
garage
|
||||
.admin_token_table
|
||||
.insert(&AdminApiToken::delete(token.prefix))
|
||||
.await?;
|
||||
|
||||
Ok(DeleteAdminTokenResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInfoResponse {
|
||||
let params = token.params().unwrap();
|
||||
|
||||
GetAdminTokenInfoResponse {
|
||||
id: Some(token.prefix.clone()),
|
||||
created: Some(
|
||||
DateTime::from_timestamp_millis(params.created as i64)
|
||||
.expect("invalid timestamp stored in db"),
|
||||
),
|
||||
name: params.name.get().to_string(),
|
||||
expiration: params.expiration.get().map(|x| {
|
||||
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expired: params.is_expired(now),
|
||||
scope: params.scope.get().0.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_existing_admin_token(garage: &Garage, id: &String) -> Result<AdminApiToken, Error> {
|
||||
garage
|
||||
.admin_token_table
|
||||
.get(&EmptyKey, id)
|
||||
.await?
|
||||
.filter(|k| !k.state.is_deleted())
|
||||
.ok_or_else(|| Error::NoSuchAdminToken(id.to_string()))
|
||||
}
|
||||
|
||||
fn apply_token_updates(
|
||||
token: &mut AdminApiToken,
|
||||
updates: UpdateAdminTokenRequestBody,
|
||||
) -> Result<(), Error> {
|
||||
if updates.never_expires && updates.expiration.is_some() {
|
||||
return Err(Error::bad_request(
|
||||
"cannot specify `expiration` and `never_expires`",
|
||||
));
|
||||
}
|
||||
|
||||
let params = token.params_mut().unwrap();
|
||||
|
||||
if let Some(name) = updates.name {
|
||||
params.name.update(name);
|
||||
}
|
||||
if let Some(expiration) = updates.expiration {
|
||||
params
|
||||
.expiration
|
||||
.update(Some(expiration.timestamp_millis() as u64));
|
||||
}
|
||||
if updates.never_expires {
|
||||
params.expiration.update(None);
|
||||
}
|
||||
if let Some(scope) = updates.scope {
|
||||
params.scope.update(AdminApiTokenScope(scope));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
1311
src/api/admin/api.rs
Normal file
1311
src/api/admin/api.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,333 +1,230 @@
|
|||
use std::collections::HashMap;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use argon2::password_hash::PasswordHash;
|
||||
|
||||
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
|
||||
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
|
||||
use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION};
|
||||
use hyper::{body::Incoming as IncomingBody, Request, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::watch;
|
||||
|
||||
use opentelemetry::trace::SpanRef;
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
use opentelemetry_prometheus::PrometheusExporter;
|
||||
#[cfg(feature = "metrics")]
|
||||
use prometheus::{Encoder, TextEncoder};
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_rpc::system::ClusterHealthStatus;
|
||||
use garage_rpc::{Endpoint as RpcEndpoint, *};
|
||||
use garage_table::EmptyKey;
|
||||
use garage_util::background::BackgroundRunner;
|
||||
use garage_util::data::Uuid;
|
||||
use garage_util::error::Error as GarageError;
|
||||
use garage_util::socket_address::UnixOrTCPSocketAddress;
|
||||
use garage_util::time::now_msec;
|
||||
|
||||
use garage_api_common::generic_server::*;
|
||||
use garage_api_common::helpers::*;
|
||||
|
||||
use crate::bucket::*;
|
||||
use crate::cluster::*;
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
use crate::key::*;
|
||||
use crate::router_v0;
|
||||
use crate::router_v1::{Authorization, Endpoint};
|
||||
use crate::router_v1;
|
||||
use crate::Authorization;
|
||||
use crate::RequestHandler;
|
||||
|
||||
// ---- FOR RPC ----
|
||||
|
||||
pub const ADMIN_RPC_PATH: &str = "garage_api/admin/rpc.rs/Rpc";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum AdminRpc {
|
||||
Proxy(AdminApiRequest),
|
||||
Internal(LocalAdminApiRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum AdminRpcResponse {
|
||||
ProxyApiOkResponse(TaggedAdminApiResponse),
|
||||
InternalApiOkResponse(LocalAdminApiResponse),
|
||||
ApiErrorResponse {
|
||||
http_code: u16,
|
||||
error_code: String,
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Rpc for AdminRpc {
|
||||
type Response = Result<AdminRpcResponse, GarageError>;
|
||||
}
|
||||
|
||||
impl EndpointHandler<AdminRpc> for AdminApiServer {
|
||||
async fn handle(
|
||||
self: &Arc<Self>,
|
||||
message: &AdminRpc,
|
||||
_from: NodeID,
|
||||
) -> Result<AdminRpcResponse, GarageError> {
|
||||
match message {
|
||||
AdminRpc::Proxy(req) => {
|
||||
info!("Proxied admin API request: {}", req.name());
|
||||
let res = req.clone().handle(&self.garage, &self).await;
|
||||
match res {
|
||||
Ok(res) => Ok(AdminRpcResponse::ProxyApiOkResponse(res.tagged())),
|
||||
Err(e) => Ok(AdminRpcResponse::ApiErrorResponse {
|
||||
http_code: e.http_status_code().as_u16(),
|
||||
error_code: e.code().to_string(),
|
||||
message: e.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
AdminRpc::Internal(req) => {
|
||||
info!("Internal admin API request: {}", req.name());
|
||||
let res = req.clone().handle(&self.garage, &self).await;
|
||||
match res {
|
||||
Ok(res) => Ok(AdminRpcResponse::InternalApiOkResponse(res)),
|
||||
Err(e) => Ok(AdminRpcResponse::ApiErrorResponse {
|
||||
http_code: e.http_status_code().as_u16(),
|
||||
error_code: e.code().to_string(),
|
||||
message: e.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- FOR HTTP ----
|
||||
|
||||
pub type ResBody = BoxBody<Error>;
|
||||
|
||||
pub struct AdminApiServer {
|
||||
garage: Arc<Garage>,
|
||||
#[cfg(feature = "metrics")]
|
||||
exporter: PrometheusExporter,
|
||||
pub(crate) exporter: PrometheusExporter,
|
||||
metrics_token: Option<String>,
|
||||
metrics_require_token: bool,
|
||||
admin_token: Option<String>,
|
||||
pub(crate) background: Arc<BackgroundRunner>,
|
||||
pub(crate) endpoint: Arc<RpcEndpoint<AdminRpc, Self>>,
|
||||
}
|
||||
|
||||
pub enum HttpEndpoint {
|
||||
Old(router_v1::Endpoint),
|
||||
New(String),
|
||||
}
|
||||
|
||||
impl AdminApiServer {
|
||||
pub fn new(
|
||||
garage: Arc<Garage>,
|
||||
background: Arc<BackgroundRunner>,
|
||||
#[cfg(feature = "metrics")] exporter: PrometheusExporter,
|
||||
) -> Self {
|
||||
) -> Arc<Self> {
|
||||
let cfg = &garage.config.admin;
|
||||
let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token);
|
||||
let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token);
|
||||
Self {
|
||||
let metrics_require_token = cfg.metrics_require_token;
|
||||
|
||||
let endpoint = garage.system.netapp.endpoint(ADMIN_RPC_PATH.into());
|
||||
let admin = Arc::new(Self {
|
||||
garage,
|
||||
#[cfg(feature = "metrics")]
|
||||
exporter,
|
||||
metrics_token,
|
||||
metrics_require_token,
|
||||
admin_token,
|
||||
}
|
||||
background,
|
||||
endpoint,
|
||||
});
|
||||
admin.endpoint.set_handler(admin.clone());
|
||||
admin
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
self,
|
||||
self: Arc<Self>,
|
||||
bind_addr: UnixOrTCPSocketAddress,
|
||||
must_exit: watch::Receiver<bool>,
|
||||
) -> Result<(), GarageError> {
|
||||
let region = self.garage.config.s3_api.s3_region.clone();
|
||||
ApiServer::new(region, self)
|
||||
ApiServer::new(region, ArcAdminApiServer(self))
|
||||
.run_server(bind_addr, Some(0o220), must_exit)
|
||||
.await
|
||||
}
|
||||
|
||||
fn handle_options(&self, _req: &Request<IncomingBody>) -> Result<Response<ResBody>, Error> {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.header(ALLOW, "OPTIONS, GET, POST")
|
||||
.header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST")
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.body(empty_body())?)
|
||||
}
|
||||
|
||||
async fn handle_check_domain(
|
||||
async fn handle_http_api(
|
||||
&self,
|
||||
req: Request<IncomingBody>,
|
||||
endpoint: HttpEndpoint,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let query_params: HashMap<String, String> = req
|
||||
.uri()
|
||||
.query()
|
||||
.map(|v| {
|
||||
url::form_urlencoded::parse(v.as_bytes())
|
||||
.into_owned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(HashMap::new);
|
||||
let auth_header = req.headers().get(AUTHORIZATION).cloned();
|
||||
|
||||
let has_domain_key = query_params.contains_key("domain");
|
||||
|
||||
if !has_domain_key {
|
||||
return Err(Error::bad_request("No domain query string found"));
|
||||
}
|
||||
|
||||
let domain = query_params
|
||||
.get("domain")
|
||||
.ok_or_internal_error("Could not parse domain query string")?;
|
||||
|
||||
if self.check_domain(domain).await? {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(string_body(format!(
|
||||
"Domain '{domain}' is managed by Garage"
|
||||
)))?)
|
||||
} else {
|
||||
Err(Error::bad_request(format!(
|
||||
"Domain '{domain}' is not managed by Garage"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_domain(&self, domain: &str) -> Result<bool, Error> {
|
||||
// Resolve bucket from domain name, inferring if the website must be activated for the
|
||||
// domain to be valid.
|
||||
let (bucket_name, must_check_website) = if let Some(bname) = self
|
||||
.garage
|
||||
.config
|
||||
.s3_api
|
||||
.root_domain
|
||||
.as_ref()
|
||||
.and_then(|rd| host_to_bucket(domain, rd))
|
||||
{
|
||||
(bname.to_string(), false)
|
||||
} else if let Some(bname) = self
|
||||
.garage
|
||||
.config
|
||||
.s3_web
|
||||
.as_ref()
|
||||
.and_then(|sw| host_to_bucket(domain, sw.root_domain.as_str()))
|
||||
{
|
||||
(bname.to_string(), true)
|
||||
} else {
|
||||
(domain.to_string(), true)
|
||||
let request = match endpoint {
|
||||
HttpEndpoint::Old(endpoint_v1) => AdminApiRequest::from_v1(endpoint_v1, req).await?,
|
||||
HttpEndpoint::New(_) => AdminApiRequest::from_request(req).await?,
|
||||
};
|
||||
|
||||
let bucket_id = match self
|
||||
.garage
|
||||
.bucket_helper()
|
||||
.resolve_global_bucket_name(&bucket_name)
|
||||
.await?
|
||||
{
|
||||
Some(bucket_id) => bucket_id,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
if !must_check_website {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let bucket = self
|
||||
.garage
|
||||
.bucket_helper()
|
||||
.get_existing_bucket(bucket_id)
|
||||
.await?;
|
||||
|
||||
let bucket_state = bucket.state.as_option().unwrap();
|
||||
let bucket_website_config = bucket_state.website_config.get();
|
||||
|
||||
match bucket_website_config {
|
||||
Some(_v) => Ok(true),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_health(&self) -> Result<Response<ResBody>, Error> {
|
||||
let health = self.garage.system.health();
|
||||
|
||||
let (status, status_str) = match health.status {
|
||||
ClusterHealthStatus::Healthy => (StatusCode::OK, "Garage is fully operational"),
|
||||
ClusterHealthStatus::Degraded => (
|
||||
StatusCode::OK,
|
||||
"Garage is operational but some storage nodes are unavailable",
|
||||
),
|
||||
ClusterHealthStatus::Unavailable => (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"Quorum is not available for some/all partitions, reads and writes will fail",
|
||||
let (global_token_hash, token_required) = match request.authorization_type() {
|
||||
Authorization::None => (None, false),
|
||||
Authorization::MetricsToken => (
|
||||
self.metrics_token.as_deref(),
|
||||
self.metrics_token.is_some() || self.metrics_require_token,
|
||||
),
|
||||
Authorization::AdminToken => (self.admin_token.as_deref(), true),
|
||||
};
|
||||
let status_str = format!(
|
||||
"{}\nConsult the full health check API endpoint at /v1/health for more details\n",
|
||||
status_str
|
||||
);
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(status)
|
||||
.header(http::header::CONTENT_TYPE, "text/plain")
|
||||
.body(string_body(status_str))?)
|
||||
if token_required {
|
||||
verify_authorization(&self.garage, global_token_hash, auth_header, request.name())?;
|
||||
}
|
||||
|
||||
fn handle_metrics(&self) -> Result<Response<ResBody>, Error> {
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
use opentelemetry::trace::Tracer;
|
||||
|
||||
let mut buffer = vec![];
|
||||
let encoder = TextEncoder::new();
|
||||
|
||||
let tracer = opentelemetry::global::tracer("garage");
|
||||
let metric_families = tracer.in_span("admin/gather_metrics", |_| {
|
||||
self.exporter.registry().gather()
|
||||
});
|
||||
|
||||
encoder
|
||||
.encode(&metric_families, &mut buffer)
|
||||
.ok_or_internal_error("Could not serialize metrics")?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(http::header::CONTENT_TYPE, encoder.format_type())
|
||||
.body(bytes_body(buffer.into()))?)
|
||||
match request {
|
||||
AdminApiRequest::Options(req) => req.handle(&self.garage, &self).await,
|
||||
AdminApiRequest::CheckDomain(req) => req.handle(&self.garage, &self).await,
|
||||
AdminApiRequest::Health(req) => req.handle(&self.garage, &self).await,
|
||||
AdminApiRequest::Metrics(req) => req.handle(&self.garage, &self).await,
|
||||
req => {
|
||||
let res = req.handle(&self.garage, &self).await?;
|
||||
let mut res = json_ok_response(&res)?;
|
||||
res.headers_mut()
|
||||
.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "metrics"))]
|
||||
Err(Error::bad_request(
|
||||
"Garage was built without the metrics feature".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiHandler for AdminApiServer {
|
||||
struct ArcAdminApiServer(Arc<AdminApiServer>);
|
||||
|
||||
impl ApiHandler for ArcAdminApiServer {
|
||||
const API_NAME: &'static str = "admin";
|
||||
const API_NAME_DISPLAY: &'static str = "Admin";
|
||||
|
||||
type Endpoint = Endpoint;
|
||||
type Endpoint = HttpEndpoint;
|
||||
type Error = Error;
|
||||
|
||||
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<Endpoint, Error> {
|
||||
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<HttpEndpoint, Error> {
|
||||
if req.uri().path().starts_with("/v0/") {
|
||||
let endpoint_v0 = router_v0::Endpoint::from_request(req)?;
|
||||
Endpoint::from_v0(endpoint_v0)
|
||||
let endpoint_v1 = router_v1::Endpoint::from_v0(endpoint_v0)?;
|
||||
Ok(HttpEndpoint::Old(endpoint_v1))
|
||||
} else if req.uri().path().starts_with("/v1/") {
|
||||
let endpoint_v1 = router_v1::Endpoint::from_request(req)?;
|
||||
Ok(HttpEndpoint::Old(endpoint_v1))
|
||||
} else {
|
||||
Endpoint::from_request(req)
|
||||
Ok(HttpEndpoint::New(req.uri().path().to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
req: Request<IncomingBody>,
|
||||
endpoint: Endpoint,
|
||||
endpoint: HttpEndpoint,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let required_auth_hash =
|
||||
match endpoint.authorization_type() {
|
||||
Authorization::None => None,
|
||||
Authorization::MetricsToken => self.metrics_token.as_deref(),
|
||||
Authorization::AdminToken => match self.admin_token.as_deref() {
|
||||
None => return Err(Error::forbidden(
|
||||
"Admin token isn't configured, admin API access is disabled for security.",
|
||||
)),
|
||||
Some(t) => Some(t),
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(password_hash) = required_auth_hash {
|
||||
match req.headers().get("Authorization") {
|
||||
None => return Err(Error::forbidden("Authorization token must be provided")),
|
||||
Some(authorization) => {
|
||||
verify_bearer_token(&authorization, password_hash)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match endpoint {
|
||||
Endpoint::Options => self.handle_options(&req),
|
||||
Endpoint::CheckDomain => self.handle_check_domain(req).await,
|
||||
Endpoint::Health => self.handle_health(),
|
||||
Endpoint::Metrics => self.handle_metrics(),
|
||||
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,
|
||||
Endpoint::GetClusterHealth => handle_get_cluster_health(&self.garage).await,
|
||||
Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await,
|
||||
// Layout
|
||||
Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await,
|
||||
Endpoint::UpdateClusterLayout => handle_update_cluster_layout(&self.garage, req).await,
|
||||
Endpoint::ApplyClusterLayout => handle_apply_cluster_layout(&self.garage, req).await,
|
||||
Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage).await,
|
||||
// Keys
|
||||
Endpoint::ListKeys => handle_list_keys(&self.garage).await,
|
||||
Endpoint::GetKeyInfo {
|
||||
id,
|
||||
search,
|
||||
show_secret_key,
|
||||
} => {
|
||||
let show_secret_key = show_secret_key.map(|x| x == "true").unwrap_or(false);
|
||||
handle_get_key_info(&self.garage, id, search, show_secret_key).await
|
||||
}
|
||||
Endpoint::CreateKey => handle_create_key(&self.garage, req).await,
|
||||
Endpoint::ImportKey => handle_import_key(&self.garage, req).await,
|
||||
Endpoint::UpdateKey { id } => handle_update_key(&self.garage, id, req).await,
|
||||
Endpoint::DeleteKey { id } => handle_delete_key(&self.garage, id).await,
|
||||
// Buckets
|
||||
Endpoint::ListBuckets => handle_list_buckets(&self.garage).await,
|
||||
Endpoint::GetBucketInfo { id, global_alias } => {
|
||||
handle_get_bucket_info(&self.garage, id, global_alias).await
|
||||
}
|
||||
Endpoint::CreateBucket => handle_create_bucket(&self.garage, req).await,
|
||||
Endpoint::DeleteBucket { id } => handle_delete_bucket(&self.garage, id).await,
|
||||
Endpoint::UpdateBucket { id } => handle_update_bucket(&self.garage, id, req).await,
|
||||
// Bucket-key permissions
|
||||
Endpoint::BucketAllowKey => {
|
||||
handle_bucket_change_key_perm(&self.garage, req, true).await
|
||||
}
|
||||
Endpoint::BucketDenyKey => {
|
||||
handle_bucket_change_key_perm(&self.garage, req, false).await
|
||||
}
|
||||
// Bucket aliasing
|
||||
Endpoint::GlobalAliasBucket { id, alias } => {
|
||||
handle_global_alias_bucket(&self.garage, id, alias).await
|
||||
}
|
||||
Endpoint::GlobalUnaliasBucket { id, alias } => {
|
||||
handle_global_unalias_bucket(&self.garage, id, alias).await
|
||||
}
|
||||
Endpoint::LocalAliasBucket {
|
||||
id,
|
||||
access_key_id,
|
||||
alias,
|
||||
} => handle_local_alias_bucket(&self.garage, id, access_key_id, alias).await,
|
||||
Endpoint::LocalUnaliasBucket {
|
||||
id,
|
||||
access_key_id,
|
||||
alias,
|
||||
} => handle_local_unalias_bucket(&self.garage, id, access_key_id, alias).await,
|
||||
}
|
||||
self.0.handle_http_api(req, endpoint).await
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiEndpoint for Endpoint {
|
||||
fn name(&self) -> &'static str {
|
||||
Endpoint::name(self)
|
||||
impl ApiEndpoint for HttpEndpoint {
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
Self::Old(endpoint_v1) => Cow::Borrowed(endpoint_v1.name()),
|
||||
Self::New(path) => Cow::Owned(path.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_span_attributes(&self, _span: SpanRef<'_>) {}
|
||||
|
|
@ -347,20 +244,87 @@ fn hash_bearer_token(token: &str) -> String {
|
|||
.to_string()
|
||||
}
|
||||
|
||||
fn verify_bearer_token(token: &hyper::http::HeaderValue, password_hash: &str) -> Result<(), Error> {
|
||||
use argon2::{password_hash::PasswordVerifier, Argon2};
|
||||
fn verify_authorization(
|
||||
garage: &Garage,
|
||||
global_token_hash: Option<&str>,
|
||||
auth_header: Option<hyper::http::HeaderValue>,
|
||||
endpoint_name: &str,
|
||||
) -> Result<(), Error> {
|
||||
use argon2::{password_hash::PasswordHash, password_hash::PasswordVerifier, Argon2};
|
||||
|
||||
let parsed_hash = PasswordHash::new(&password_hash).unwrap();
|
||||
let invalid_msg = "Invalid bearer token";
|
||||
|
||||
token
|
||||
let token = match &auth_header {
|
||||
None => {
|
||||
return Err(Error::forbidden(
|
||||
"Bearer token must be provided in Authorization header",
|
||||
))
|
||||
}
|
||||
Some(authorization) => authorization
|
||||
.to_str()?
|
||||
.strip_prefix("Bearer ")
|
||||
.and_then(|token| {
|
||||
.ok_or_else(|| Error::forbidden("Invalid Authorization header"))?
|
||||
.trim(),
|
||||
};
|
||||
|
||||
let token_hash_string = if let Some((prefix, _)) = token.split_once('.') {
|
||||
garage
|
||||
.admin_token_table
|
||||
.get_local(&EmptyKey, &prefix.to_string())?
|
||||
.and_then(|k| k.state.into_option())
|
||||
.filter(|p| !p.is_expired(now_msec()))
|
||||
.filter(|p| p.has_scope(endpoint_name))
|
||||
.ok_or_else(|| Error::forbidden(invalid_msg))?
|
||||
.token_hash
|
||||
} else {
|
||||
global_token_hash
|
||||
.ok_or_else(|| Error::forbidden(invalid_msg))?
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let token_hash =
|
||||
PasswordHash::new(&token_hash_string).ok_or_internal_error("Could not parse token hash")?;
|
||||
|
||||
Argon2::default()
|
||||
.verify_password(token.trim().as_bytes(), &parsed_hash)
|
||||
.ok()
|
||||
})
|
||||
.ok_or_else(|| Error::forbidden("Invalid authorization token"))?;
|
||||
.verify_password(token.as_bytes(), &token_hash)
|
||||
.map_err(|_| Error::forbidden(invalid_msg))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn find_matching_nodes(garage: &Garage, spec: &str) -> Result<Vec<Uuid>, Error> {
|
||||
let mut res = vec![];
|
||||
if spec == "*" {
|
||||
res = garage.system.cluster_layout().all_nodes().to_vec();
|
||||
for node in garage.system.get_known_nodes() {
|
||||
if node.is_up && !res.contains(&node.id) {
|
||||
res.push(node.id);
|
||||
}
|
||||
}
|
||||
} else if spec == "self" {
|
||||
res.push(garage.system.id);
|
||||
} else {
|
||||
let layout = garage.system.cluster_layout();
|
||||
let known_nodes = garage.system.get_known_nodes();
|
||||
let all_nodes = layout
|
||||
.all_nodes()
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(known_nodes.iter().filter(|x| x.is_up).map(|x| x.id));
|
||||
for node in all_nodes {
|
||||
if !res.contains(&node) && hex::encode(node).starts_with(spec) {
|
||||
res.push(node);
|
||||
}
|
||||
}
|
||||
if res.is_empty() {
|
||||
return Err(Error::bad_request(format!("No nodes matching {}", spec)));
|
||||
}
|
||||
if res.len() > 1 {
|
||||
return Err(Error::bad_request(format!(
|
||||
"Multiple nodes matching {}: {:?}",
|
||||
spec, res
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
|
|
|||
276
src/api/admin/block.rs
Normal file
276
src/api/admin/block.rs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::Error as GarageError;
|
||||
use garage_util::time::now_msec;
|
||||
|
||||
use garage_table::EmptyKey;
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::s3::object_table::*;
|
||||
use garage_model::s3::version_table::*;
|
||||
|
||||
use garage_api_common::common_error::CommonErrorDerivative;
|
||||
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
use crate::{Admin, RequestHandler};
|
||||
|
||||
impl RequestHandler for LocalListBlockErrorsRequest {
|
||||
type Response = LocalListBlockErrorsResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<LocalListBlockErrorsResponse, Error> {
|
||||
let errors = garage.block_manager.list_resync_errors()?;
|
||||
let now = now_msec();
|
||||
let errors = errors
|
||||
.into_iter()
|
||||
.map(|e| BlockError {
|
||||
block_hash: hex::encode(&e.hash),
|
||||
refcount: e.refcount,
|
||||
error_count: e.error_count,
|
||||
last_try_secs_ago: now.saturating_sub(e.last_try) / 1000,
|
||||
next_try_in_secs: e.next_try.saturating_sub(now) / 1000,
|
||||
})
|
||||
.collect();
|
||||
Ok(LocalListBlockErrorsResponse(errors))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for LocalGetBlockInfoRequest {
|
||||
type Response = LocalGetBlockInfoResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<LocalGetBlockInfoResponse, Error> {
|
||||
let hash = find_block_hash_by_prefix(garage, &self.block_hash)?;
|
||||
let refcount = garage.block_manager.get_block_rc(&hash)?;
|
||||
let block_refs = garage
|
||||
.block_ref_table
|
||||
.get_range(&hash, None, None, 10000, Default::default())
|
||||
.await?;
|
||||
let mut versions = vec![];
|
||||
for br in block_refs {
|
||||
if let Some(v) = garage.version_table.get(&br.version, &EmptyKey).await? {
|
||||
let bl = match &v.backlink {
|
||||
VersionBacklink::MultipartUpload { upload_id } => {
|
||||
if let Some(u) = garage.mpu_table.get(upload_id, &EmptyKey).await? {
|
||||
BlockVersionBacklink::Upload {
|
||||
upload_id: hex::encode(&upload_id),
|
||||
upload_deleted: u.deleted.get(),
|
||||
upload_garbage_collected: false,
|
||||
bucket_id: Some(hex::encode(&u.bucket_id)),
|
||||
key: Some(u.key.to_string()),
|
||||
}
|
||||
} else {
|
||||
BlockVersionBacklink::Upload {
|
||||
upload_id: hex::encode(&upload_id),
|
||||
upload_deleted: true,
|
||||
upload_garbage_collected: true,
|
||||
bucket_id: None,
|
||||
key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
VersionBacklink::Object { bucket_id, key } => BlockVersionBacklink::Object {
|
||||
bucket_id: hex::encode(&bucket_id),
|
||||
key: key.to_string(),
|
||||
},
|
||||
};
|
||||
versions.push(BlockVersion {
|
||||
version_id: hex::encode(&br.version),
|
||||
ref_deleted: br.deleted.get(),
|
||||
version_deleted: v.deleted.get(),
|
||||
garbage_collected: false,
|
||||
backlink: Some(bl),
|
||||
});
|
||||
} else {
|
||||
versions.push(BlockVersion {
|
||||
version_id: hex::encode(&br.version),
|
||||
ref_deleted: br.deleted.get(),
|
||||
version_deleted: true,
|
||||
garbage_collected: true,
|
||||
backlink: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(LocalGetBlockInfoResponse {
|
||||
block_hash: hex::encode(&hash),
|
||||
refcount,
|
||||
versions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for LocalRetryBlockResyncRequest {
|
||||
type Response = LocalRetryBlockResyncResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<LocalRetryBlockResyncResponse, Error> {
|
||||
match self {
|
||||
Self::All { all: true } => {
|
||||
let blocks = garage.block_manager.list_resync_errors()?;
|
||||
for b in blocks.iter() {
|
||||
garage.block_manager.resync.clear_backoff(&b.hash)?;
|
||||
}
|
||||
Ok(LocalRetryBlockResyncResponse {
|
||||
count: blocks.len() as u64,
|
||||
})
|
||||
}
|
||||
Self::All { all: false } => Err(Error::bad_request("nonsense")),
|
||||
Self::Blocks { block_hashes } => {
|
||||
for hash in block_hashes.iter() {
|
||||
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
|
||||
let hash = Hash::try_from(&hash).ok_or_bad_request("invalid hash")?;
|
||||
garage.block_manager.resync.clear_backoff(&hash)?;
|
||||
}
|
||||
Ok(LocalRetryBlockResyncResponse {
|
||||
count: block_hashes.len() as u64,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for LocalPurgeBlocksRequest {
|
||||
type Response = LocalPurgeBlocksResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<LocalPurgeBlocksResponse, Error> {
|
||||
let mut obj_dels = 0;
|
||||
let mut mpu_dels = 0;
|
||||
let mut ver_dels = 0;
|
||||
|
||||
for hash in self.0.iter() {
|
||||
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
|
||||
let hash = Hash::try_from(&hash).ok_or_bad_request("invalid hash")?;
|
||||
let block_refs = garage
|
||||
.block_ref_table
|
||||
.get_range(&hash, None, None, 10000, Default::default())
|
||||
.await?;
|
||||
|
||||
for br in block_refs {
|
||||
if let Some(version) = garage.version_table.get(&br.version, &EmptyKey).await? {
|
||||
handle_block_purge_version_backlink(
|
||||
garage,
|
||||
&version,
|
||||
&mut obj_dels,
|
||||
&mut mpu_dels,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !version.deleted.get() {
|
||||
let deleted_version = Version::new(version.uuid, version.backlink, true);
|
||||
garage.version_table.insert(&deleted_version).await?;
|
||||
ver_dels += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LocalPurgeBlocksResponse {
|
||||
blocks_purged: self.0.len() as u64,
|
||||
versions_deleted: ver_dels,
|
||||
objects_deleted: obj_dels,
|
||||
uploads_deleted: mpu_dels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn find_block_hash_by_prefix(garage: &Arc<Garage>, prefix: &str) -> Result<Hash, Error> {
|
||||
if prefix.len() < 4 {
|
||||
return Err(Error::bad_request(
|
||||
"Please specify at least 4 characters of the block hash",
|
||||
));
|
||||
}
|
||||
|
||||
let prefix_bin = hex::decode(&prefix[..prefix.len() & !1]).ok_or_bad_request("invalid hash")?;
|
||||
|
||||
let iter = garage
|
||||
.block_ref_table
|
||||
.data
|
||||
.store
|
||||
.range(&prefix_bin[..]..)
|
||||
.map_err(GarageError::from)?;
|
||||
let mut found = None;
|
||||
for item in iter {
|
||||
let (k, _v) = item.map_err(GarageError::from)?;
|
||||
let hash = Hash::try_from(&k[..32]).unwrap();
|
||||
if &hash.as_slice()[..prefix_bin.len()] != prefix_bin {
|
||||
break;
|
||||
}
|
||||
if hex::encode(hash.as_slice()).starts_with(prefix) {
|
||||
match &found {
|
||||
Some(x) if *x == hash => (),
|
||||
Some(_) => {
|
||||
return Err(Error::bad_request(format!(
|
||||
"Several blocks match prefix `{}`",
|
||||
prefix
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
found = Some(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
found.ok_or_else(|| Error::NoSuchBlock(prefix.to_string()))
|
||||
}
|
||||
|
||||
async fn handle_block_purge_version_backlink(
|
||||
garage: &Arc<Garage>,
|
||||
version: &Version,
|
||||
obj_dels: &mut u64,
|
||||
mpu_dels: &mut u64,
|
||||
) -> Result<(), Error> {
|
||||
let (bucket_id, key, ov_id) = match &version.backlink {
|
||||
VersionBacklink::Object { bucket_id, key } => (*bucket_id, key.clone(), version.uuid),
|
||||
VersionBacklink::MultipartUpload { upload_id } => {
|
||||
if let Some(mut mpu) = garage.mpu_table.get(upload_id, &EmptyKey).await? {
|
||||
if !mpu.deleted.get() {
|
||||
mpu.parts.clear();
|
||||
mpu.deleted.set();
|
||||
garage.mpu_table.insert(&mpu).await?;
|
||||
*mpu_dels += 1;
|
||||
}
|
||||
(mpu.bucket_id, mpu.key.clone(), *upload_id)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(object) = garage.object_table.get(&bucket_id, &key).await? {
|
||||
let ov = object.versions().iter().rev().find(|v| v.is_complete());
|
||||
if let Some(ov) = ov {
|
||||
if ov.uuid == ov_id {
|
||||
let del_uuid = gen_uuid();
|
||||
let deleted_object = Object::new(
|
||||
bucket_id,
|
||||
key,
|
||||
vec![ObjectVersion {
|
||||
uuid: del_uuid,
|
||||
timestamp: ov.timestamp + 1,
|
||||
state: ObjectVersionState::Complete(ObjectVersionData::DeleteMarker),
|
||||
}],
|
||||
);
|
||||
garage.object_table.insert(&deleted_object).await?;
|
||||
*obj_dels += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue