mirror of
https://git.deuxfleurs.fr/Deuxfleurs/garage.git
synced 2026-11-12 09:09:15 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
713c61acb8 |
||
|
|
1c88ee9bc5 |
||
|
|
d768f559da |
||
|
|
99976e11f8 |
||
|
|
5367f8adb2 |
||
|
|
30e393b439 |
||
|
|
926b3c0fad |
||
|
|
dcfa408887 |
||
|
|
70383b4363 |
||
|
|
440a577563 |
||
|
|
5072dbd228 |
||
|
|
2ce3513c10 |
||
|
|
e92c52eb65 |
||
|
|
8b1338ef2f |
||
|
|
8ff95f09c9 |
||
|
|
86a3fe8ec5 |
||
|
|
ec50ffac42 |
||
|
|
d7736cb614 |
||
|
|
8033bdb0b4 |
||
|
|
5a535788fc |
||
|
|
ea325d78d3 |
||
|
|
ec16d166f9 |
||
|
|
7a5d329e49 |
||
|
|
f82b938033 |
||
|
|
96b11524d5 |
||
|
|
c0fb9fd0fe |
||
|
|
983037d965 |
||
|
|
e4e1f8f0d6 |
||
|
|
e7ddba53e3 |
||
|
|
ed76893581 |
||
|
|
fc2f73ddb5 |
||
|
|
fe399a3265 |
||
|
|
de1a5b87b6 |
||
|
|
2b93a01d2b |
||
|
|
aeb978552a |
||
|
|
393b76ecba |
||
|
|
5c00c9fb46 |
||
|
|
f97a7845e9 |
||
|
|
bb6ec9ebd9 |
||
|
|
dd54d0b2b1 |
||
|
|
01c4876fb4 |
||
|
|
e4c61124d8 |
||
|
|
ec03e3d16c |
||
|
|
7a19daafbd |
||
|
|
99fcfa3844 |
||
|
|
633958c7b1 |
66 changed files with 3240 additions and 986 deletions
28
Cargo.lock
generated
28
Cargo.lock
generated
|
|
@ -888,21 +888,23 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"garage_admin",
|
|
||||||
"garage_api",
|
"garage_api",
|
||||||
"garage_model 0.7.0",
|
"garage_model 0.7.0",
|
||||||
"garage_rpc 0.7.0",
|
"garage_rpc 0.7.0",
|
||||||
"garage_table 0.7.0",
|
"garage_table 0.7.0",
|
||||||
"garage_util 0.7.0",
|
"garage_util 0.7.0",
|
||||||
"garage_web",
|
"garage_web",
|
||||||
"git-version",
|
|
||||||
"hex",
|
"hex",
|
||||||
"hmac 0.10.1",
|
"hmac 0.10.1",
|
||||||
"http",
|
"http",
|
||||||
"hyper",
|
"hyper",
|
||||||
"kuska-sodiumoxide",
|
"kuska-sodiumoxide",
|
||||||
"netapp 0.4.4",
|
"netapp 0.4.4",
|
||||||
|
"opentelemetry",
|
||||||
|
"opentelemetry-otlp",
|
||||||
|
"opentelemetry-prometheus",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
|
"prometheus",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rmp-serde 0.15.5",
|
"rmp-serde 0.15.5",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -917,23 +919,6 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "garage_admin"
|
|
||||||
version = "0.7.0"
|
|
||||||
dependencies = [
|
|
||||||
"futures",
|
|
||||||
"futures-util",
|
|
||||||
"garage_util 0.7.0",
|
|
||||||
"hex",
|
|
||||||
"http",
|
|
||||||
"hyper",
|
|
||||||
"opentelemetry",
|
|
||||||
"opentelemetry-otlp",
|
|
||||||
"opentelemetry-prometheus",
|
|
||||||
"prometheus",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_api"
|
name = "garage_api"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
@ -963,8 +948,11 @@ dependencies = [
|
||||||
"multer",
|
"multer",
|
||||||
"nom",
|
"nom",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
|
"opentelemetry-otlp",
|
||||||
|
"opentelemetry-prometheus",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project 1.0.10",
|
"pin-project 1.0.10",
|
||||||
|
"prometheus",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1089,9 +1077,9 @@ dependencies = [
|
||||||
"bytes 1.1.0",
|
"bytes 1.1.0",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"garage_admin",
|
|
||||||
"garage_util 0.7.0",
|
"garage_util 0.7.0",
|
||||||
"gethostname",
|
"gethostname",
|
||||||
|
"git-version",
|
||||||
"hex",
|
"hex",
|
||||||
"hyper",
|
"hyper",
|
||||||
"k8s-openapi",
|
"k8s-openapi",
|
||||||
|
|
|
||||||
193
Cargo.nix
193
Cargo.nix
|
|
@ -6,7 +6,6 @@ args@{
|
||||||
rootFeatures ? [
|
rootFeatures ? [
|
||||||
"garage_util/default"
|
"garage_util/default"
|
||||||
"garage_rpc/default"
|
"garage_rpc/default"
|
||||||
"garage_admin/default"
|
|
||||||
"garage_table/default"
|
"garage_table/default"
|
||||||
"garage_block/default"
|
"garage_block/default"
|
||||||
"garage_model/default"
|
"garage_model/default"
|
||||||
|
|
@ -48,7 +47,6 @@ in
|
||||||
workspace = {
|
workspace = {
|
||||||
garage_util = rustPackages.unknown.garage_util."0.7.0";
|
garage_util = rustPackages.unknown.garage_util."0.7.0";
|
||||||
garage_rpc = rustPackages.unknown.garage_rpc."0.7.0";
|
garage_rpc = rustPackages.unknown.garage_rpc."0.7.0";
|
||||||
garage_admin = rustPackages.unknown.garage_admin."0.7.0";
|
|
||||||
garage_table = rustPackages.unknown.garage_table."0.7.0";
|
garage_table = rustPackages.unknown.garage_table."0.7.0";
|
||||||
garage_block = rustPackages.unknown.garage_block."0.7.0";
|
garage_block = rustPackages.unknown.garage_block."0.7.0";
|
||||||
garage_model = rustPackages.unknown.garage_model."0.7.0";
|
garage_model = rustPackages.unknown.garage_model."0.7.0";
|
||||||
|
|
@ -1220,7 +1218,7 @@ in
|
||||||
[ "async-await" ]
|
[ "async-await" ]
|
||||||
[ "async-await-macro" ]
|
[ "async-await-macro" ]
|
||||||
[ "channel" ]
|
[ "channel" ]
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "default")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "default")
|
||||||
[ "futures-channel" ]
|
[ "futures-channel" ]
|
||||||
[ "futures-io" ]
|
[ "futures-io" ]
|
||||||
[ "futures-macro" ]
|
[ "futures-macro" ]
|
||||||
|
|
@ -1269,18 +1267,20 @@ in
|
||||||
bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; };
|
bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; };
|
||||||
futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; };
|
futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; };
|
||||||
futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; };
|
futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; };
|
||||||
garage_admin = rustPackages."unknown".garage_admin."0.7.0" { inherit profileName; };
|
|
||||||
garage_api = rustPackages."unknown".garage_api."0.7.0" { inherit profileName; };
|
garage_api = rustPackages."unknown".garage_api."0.7.0" { inherit profileName; };
|
||||||
garage_model = rustPackages."unknown".garage_model."0.7.0" { inherit profileName; };
|
garage_model = rustPackages."unknown".garage_model."0.7.0" { inherit profileName; };
|
||||||
garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; };
|
garage_rpc = rustPackages."unknown".garage_rpc."0.7.0" { inherit profileName; };
|
||||||
garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; };
|
garage_table = rustPackages."unknown".garage_table."0.7.0" { inherit profileName; };
|
||||||
garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; };
|
garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; };
|
||||||
garage_web = rustPackages."unknown".garage_web."0.7.0" { inherit profileName; };
|
garage_web = rustPackages."unknown".garage_web."0.7.0" { inherit profileName; };
|
||||||
git_version = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; };
|
|
||||||
hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; };
|
hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; };
|
||||||
sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; };
|
sodiumoxide = rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; };
|
||||||
netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; };
|
netapp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.4.4" { inherit profileName; };
|
||||||
|
opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; };
|
||||||
|
opentelemetry_otlp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; };
|
||||||
|
opentelemetry_prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; };
|
||||||
pretty_env_logger = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; };
|
pretty_env_logger = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; };
|
||||||
|
prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prometheus."0.13.0" { inherit profileName; };
|
||||||
rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; };
|
rand = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; };
|
||||||
rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; };
|
rmp_serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; };
|
||||||
serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; };
|
serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; };
|
||||||
|
|
@ -1305,26 +1305,6 @@ in
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"unknown".garage_admin."0.7.0" = overridableMkRustCrate (profileName: rec {
|
|
||||||
name = "garage_admin";
|
|
||||||
version = "0.7.0";
|
|
||||||
registry = "unknown";
|
|
||||||
src = fetchCrateLocal (workspaceSrc + "/src/admin");
|
|
||||||
dependencies = {
|
|
||||||
futures = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; };
|
|
||||||
futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; };
|
|
||||||
garage_util = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; };
|
|
||||||
hex = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; };
|
|
||||||
http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; };
|
|
||||||
hyper = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; };
|
|
||||||
opentelemetry = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; };
|
|
||||||
opentelemetry_otlp = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; };
|
|
||||||
opentelemetry_prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; };
|
|
||||||
prometheus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prometheus."0.13.0" { inherit profileName; };
|
|
||||||
tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; };
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
"unknown".garage_api."0.7.0" = overridableMkRustCrate (profileName: rec {
|
"unknown".garage_api."0.7.0" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "garage_api";
|
name = "garage_api";
|
||||||
version = "0.7.0";
|
version = "0.7.0";
|
||||||
|
|
@ -1359,8 +1339,11 @@ in
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "multer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".multer."2.0.2" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "multer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".multer."2.0.2" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "nom" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.1" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "nom" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.1" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; };
|
||||||
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry_otlp" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-otlp."0.10.0" { inherit profileName; };
|
||||||
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "opentelemetry_prometheus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "percent_encoding" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "percent_encoding" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.1.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; };
|
||||||
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "prometheus" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".prometheus."0.13.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "quick_xml" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.21.0" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "quick_xml" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.21.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "roxmltree" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".roxmltree."0.14.1" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "roxmltree" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".roxmltree."0.14.1" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "serde" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.137" { inherit profileName; };
|
||||||
|
|
@ -1506,9 +1489,9 @@ in
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "bytes" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "garage_admin" else null } = rustPackages."unknown".garage_admin."0.7.0" { inherit profileName; };
|
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "garage_util" else null } = rustPackages."unknown".garage_util."0.7.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "gethostname" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".gethostname."0.2.3" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "gethostname" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".gethostname."0.2.3" { inherit profileName; };
|
||||||
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "git_version" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hex" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "hyper" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc" then "k8s_openapi" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.13.1" { inherit profileName; };
|
||||||
|
|
@ -1898,31 +1881,31 @@ in
|
||||||
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
||||||
src = fetchCratesIo { inherit name version; sha256 = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2"; };
|
src = fetchCratesIo { inherit name version; sha256 = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2"; };
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "client")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "client")
|
||||||
[ "default" ]
|
[ "default" ]
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "full")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "full")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "h2")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "h2")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http1")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http1")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http2")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "http2")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "runtime")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "runtime")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "server")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "server")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "socket2")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "socket2")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "stream")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "stream")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "tcp")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "tcp")
|
||||||
];
|
];
|
||||||
dependencies = {
|
dependencies = {
|
||||||
bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; };
|
bytes = rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.1.0" { inherit profileName; };
|
||||||
futures_channel = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; };
|
futures_channel = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.21" { inherit profileName; };
|
||||||
futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; };
|
futures_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; };
|
||||||
futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; };
|
futures_util = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "h2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.12" { inherit profileName; };
|
||||||
http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; };
|
http = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.6" { inherit profileName; };
|
||||||
http_body = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.4" { inherit profileName; };
|
http_body = rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.4" { inherit profileName; };
|
||||||
httparse = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httparse."1.6.0" { inherit profileName; };
|
httparse = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httparse."1.6.0" { inherit profileName; };
|
||||||
httpdate = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."1.0.2" { inherit profileName; };
|
httpdate = rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."1.0.2" { inherit profileName; };
|
||||||
itoa = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.1" { inherit profileName; };
|
itoa = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.1" { inherit profileName; };
|
||||||
pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; };
|
pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "socket2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client" then "socket2" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; };
|
||||||
tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; };
|
tokio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; };
|
||||||
tower_service = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; };
|
tower_service = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; };
|
||||||
tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; };
|
tracing = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; };
|
||||||
|
|
@ -3341,7 +3324,7 @@ in
|
||||||
[ "getrandom" ]
|
[ "getrandom" ]
|
||||||
[ "libc" ]
|
[ "libc" ]
|
||||||
[ "rand_chacha" ]
|
[ "rand_chacha" ]
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "small_rng")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "small_rng")
|
||||||
[ "std" ]
|
[ "std" ]
|
||||||
[ "std_rng" ]
|
[ "std_rng" ]
|
||||||
];
|
];
|
||||||
|
|
@ -3434,28 +3417,28 @@ in
|
||||||
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
||||||
src = fetchCratesIo { inherit name version; sha256 = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"; };
|
src = fetchCratesIo { inherit name version; sha256 = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"; };
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
[ "aho-corasick" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "aho-corasick")
|
||||||
[ "default" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "default")
|
||||||
[ "memchr" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "memchr")
|
||||||
[ "perf" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf")
|
||||||
[ "perf-cache" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-cache")
|
||||||
[ "perf-dfa" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-dfa")
|
||||||
[ "perf-inline" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-inline")
|
||||||
[ "perf-literal" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "perf-literal")
|
||||||
[ "std" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "std")
|
||||||
[ "unicode" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode")
|
||||||
[ "unicode-age" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-age")
|
||||||
[ "unicode-bool" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-bool")
|
||||||
[ "unicode-case" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-case")
|
||||||
[ "unicode-gencat" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-gencat")
|
||||||
[ "unicode-perl" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-perl")
|
||||||
[ "unicode-script" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-script")
|
||||||
[ "unicode-segment" ]
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "unicode-segment")
|
||||||
];
|
];
|
||||||
dependencies = {
|
dependencies = {
|
||||||
aho_corasick = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "aho_corasick" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."0.7.18" { inherit profileName; };
|
||||||
memchr = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "memchr" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.4.1" { inherit profileName; };
|
||||||
regex_syntax = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "regex_syntax" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.6.25" { inherit profileName; };
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -4302,8 +4285,8 @@ in
|
||||||
[ "bytes" ]
|
[ "bytes" ]
|
||||||
[ "default" ]
|
[ "default" ]
|
||||||
[ "fs" ]
|
[ "fs" ]
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "full")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "full")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io-std")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "io-std")
|
||||||
[ "io-util" ]
|
[ "io-util" ]
|
||||||
[ "libc" ]
|
[ "libc" ]
|
||||||
[ "macros" ]
|
[ "macros" ]
|
||||||
|
|
@ -4312,8 +4295,8 @@ in
|
||||||
[ "net" ]
|
[ "net" ]
|
||||||
[ "num_cpus" ]
|
[ "num_cpus" ]
|
||||||
[ "once_cell" ]
|
[ "once_cell" ]
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "parking_lot")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "parking_lot")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "process")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "process")
|
||||||
[ "rt" ]
|
[ "rt" ]
|
||||||
[ "rt-multi-thread" ]
|
[ "rt-multi-thread" ]
|
||||||
[ "signal" ]
|
[ "signal" ]
|
||||||
|
|
@ -4331,7 +4314,7 @@ in
|
||||||
mio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".mio."0.8.2" { inherit profileName; };
|
mio = rustPackages."registry+https://github.com/rust-lang/crates.io-index".mio."0.8.2" { inherit profileName; };
|
||||||
num_cpus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num_cpus."1.13.1" { inherit profileName; };
|
num_cpus = rustPackages."registry+https://github.com/rust-lang/crates.io-index".num_cpus."1.13.1" { inherit profileName; };
|
||||||
once_cell = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; };
|
once_cell = rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.10.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.12.0" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "parking_lot" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.12.0" { inherit profileName; };
|
||||||
pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; };
|
pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; };
|
||||||
${ if hostPlatform.isUnix then "signal_hook_registry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".signal-hook-registry."1.4.0" { inherit profileName; };
|
${ if hostPlatform.isUnix then "signal_hook_registry" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".signal-hook-registry."1.4.0" { inherit profileName; };
|
||||||
socket2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; };
|
socket2 = rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.4.4" { inherit profileName; };
|
||||||
|
|
@ -4409,9 +4392,9 @@ in
|
||||||
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
||||||
src = fetchCratesIo { inherit name version; sha256 = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"; };
|
src = fetchCratesIo { inherit name version; sha256 = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"; };
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "codec")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "codec")
|
||||||
[ "compat" ]
|
[ "compat" ]
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "default")
|
||||||
[ "futures-io" ]
|
[ "futures-io" ]
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "io")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "io")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_rpc") "slab")
|
||||||
|
|
@ -4528,43 +4511,43 @@ in
|
||||||
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
||||||
src = fetchCratesIo { inherit name version; sha256 = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e"; };
|
src = fetchCratesIo { inherit name version; sha256 = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e"; };
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "__common")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "__common")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "balance")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "balance")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "buffer")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "buffer")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "default")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "default")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "discover")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "discover")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "futures-core")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "futures-core")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "futures-util")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "futures-util")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "indexmap")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "indexmap")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "limit")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "limit")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "load")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "load")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "log")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "log")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "make")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "make")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "pin-project")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "pin-project")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "pin-project-lite")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "pin-project-lite")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "rand")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "rand")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "ready-cache")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "ready-cache")
|
||||||
(lib.optional (rootFeatures' ? "garage") "retry")
|
(lib.optional (rootFeatures' ? "garage") "retry")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "slab")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "slab")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "timeout")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web") "timeout")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "tokio")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "tokio")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "tokio-util")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "tokio-util")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "tracing")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "tracing")
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "util")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "util")
|
||||||
];
|
];
|
||||||
dependencies = {
|
dependencies = {
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "futures_core" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.21" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "futures_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "indexmap" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.8.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "pin_project" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.0.10" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "pin_project_lite" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "rand" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" then "slab" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.5" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tokio" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.17.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tokio_util" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tower_layer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.1" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tower_layer" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.1" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tower_service" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tower_service" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.1" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "tracing" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; };
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -4617,14 +4600,14 @@ in
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
[ "attributes" ]
|
[ "attributes" ]
|
||||||
[ "default" ]
|
[ "default" ]
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web") "log")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web") "log")
|
||||||
(lib.optional (rootFeatures' ? "garage") "log-always")
|
(lib.optional (rootFeatures' ? "garage") "log-always")
|
||||||
[ "std" ]
|
[ "std" ]
|
||||||
[ "tracing-attributes" ]
|
[ "tracing-attributes" ]
|
||||||
];
|
];
|
||||||
dependencies = {
|
dependencies = {
|
||||||
cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; };
|
cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; };
|
||||||
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; };
|
${ if rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_web" then "log" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; };
|
||||||
pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; };
|
pin_project_lite = rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.8" { inherit profileName; };
|
||||||
tracing_attributes = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-attributes."0.1.20" { profileName = "__noProfile"; };
|
tracing_attributes = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-attributes."0.1.20" { profileName = "__noProfile"; };
|
||||||
tracing_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; };
|
tracing_core = rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.23" { inherit profileName; };
|
||||||
|
|
@ -4986,7 +4969,7 @@ in
|
||||||
[ "std" ]
|
[ "std" ]
|
||||||
[ "synchapi" ]
|
[ "synchapi" ]
|
||||||
[ "sysinfoapi" ]
|
[ "sysinfoapi" ]
|
||||||
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_admin" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_block" || rootFeatures' ? "garage_model" || rootFeatures' ? "garage_rpc" || rootFeatures' ? "garage_table" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "threadpoollegacyapiset")
|
(lib.optional (rootFeatures' ? "garage" || rootFeatures' ? "garage_api" || rootFeatures' ? "garage_web" || rootFeatures' ? "k2v-client") "threadpoollegacyapiset")
|
||||||
[ "timezoneapi" ]
|
[ "timezoneapi" ]
|
||||||
[ "winbase" ]
|
[ "winbase" ]
|
||||||
[ "wincon" ]
|
[ "wincon" ]
|
||||||
|
|
@ -5049,8 +5032,8 @@ in
|
||||||
${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; };
|
${ if hostPlatform.config == "aarch64-uwp-windows-msvc" || hostPlatform.config == "aarch64-pc-windows-msvc" then "windows_aarch64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_aarch64_msvc."0.32.0" { inherit profileName; };
|
||||||
${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; };
|
${ if hostPlatform.config == "i686-pc-windows-gnu" || hostPlatform.config == "i686-uwp-windows-gnu" then "windows_i686_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_gnu."0.32.0" { inherit profileName; };
|
||||||
${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; };
|
${ if hostPlatform.config == "i686-uwp-windows-msvc" || hostPlatform.config == "i686-pc-windows-msvc" then "windows_i686_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_i686_msvc."0.32.0" { inherit profileName; };
|
||||||
${ if hostPlatform.config == "x86_64-uwp-windows-gnu" || hostPlatform.config == "x86_64-pc-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; };
|
${ if hostPlatform.config == "x86_64-pc-windows-gnu" || hostPlatform.config == "x86_64-uwp-windows-gnu" then "windows_x86_64_gnu" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_gnu."0.32.0" { inherit profileName; };
|
||||||
${ if hostPlatform.config == "x86_64-uwp-windows-msvc" || hostPlatform.config == "x86_64-pc-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; };
|
${ if hostPlatform.config == "x86_64-pc-windows-msvc" || hostPlatform.config == "x86_64-uwp-windows-msvc" then "windows_x86_64_msvc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows_x86_64_msvc."0.32.0" { inherit profileName; };
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ members = [
|
||||||
"src/table",
|
"src/table",
|
||||||
"src/block",
|
"src/block",
|
||||||
"src/model",
|
"src/model",
|
||||||
"src/admin",
|
|
||||||
"src/api",
|
"src/api",
|
||||||
"src/web",
|
"src/web",
|
||||||
"src/garage",
|
"src/garage",
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -1,7 +1,7 @@
|
||||||
.PHONY: doc all release shell
|
.PHONY: doc all release shell
|
||||||
|
|
||||||
all:
|
all:
|
||||||
clear; cargo build --features k2v
|
clear; cargo build --all-features
|
||||||
|
|
||||||
doc:
|
doc:
|
||||||
cd doc/book; mdbook build
|
cd doc/book; mdbook build
|
||||||
|
|
|
||||||
|
|
@ -86,13 +86,14 @@ in let
|
||||||
It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.).
|
It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.).
|
||||||
But we want to ship these additional features when we release Garage.
|
But we want to ship these additional features when we release Garage.
|
||||||
In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds.
|
In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds.
|
||||||
Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate.
|
Currently, the only such feature of Garage is kubernetes-discovery from the garage_rpc crate.
|
||||||
|
The experimental feature k2v is also enabled here in all builds.
|
||||||
*/
|
*/
|
||||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||||
name = "garage_rpc";
|
name = "garage_rpc";
|
||||||
overrideArgs = old:
|
overrideArgs = old:
|
||||||
{
|
{
|
||||||
features = if release then [ "kubernetes-discovery" ] else [];
|
features = if release then [ "kubernetes-discovery" "k2v" ] else [ "k2v" ];
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
|
||||||
596
doc/drafts/admin-api.md
Normal file
596
doc/drafts/admin-api.md
Normal file
|
|
@ -0,0 +1,596 @@
|
||||||
|
# Specification of Garage's administration API
|
||||||
|
|
||||||
|
|
||||||
|
**WARNING.** At this point, there is no comittement to stability of the APIs described in this document.
|
||||||
|
We will bump the version numbers prefixed to each API endpoint at each time the syntax
|
||||||
|
or semantics change, meaning that code that relies on these endpoint will break
|
||||||
|
when changes are introduced.
|
||||||
|
|
||||||
|
|
||||||
|
## Access control
|
||||||
|
|
||||||
|
The admin API uses two different tokens for acces control, that are specified in the config file's `[admin]` section:
|
||||||
|
|
||||||
|
- `metrics_token`: the token for accessing the Metrics endpoint (if this token is not set in the config file, the Metrics endpoint can be accessed without access control);
|
||||||
|
- `admin_token`: the token for accessing all of the other administration endpoints (if this token is not set in the config file, these endpoints can be accessed without access control).
|
||||||
|
|
||||||
|
## Administration API endpoints
|
||||||
|
|
||||||
|
### Metrics-related endpoints
|
||||||
|
|
||||||
|
#### Metrics `GET /metrics`
|
||||||
|
|
||||||
|
Returns internal Garage metrics in Prometheus format.
|
||||||
|
|
||||||
|
### Cluster operations
|
||||||
|
|
||||||
|
#### GetClusterStatus `GET /v0/status`
|
||||||
|
|
||||||
|
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": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
|
||||||
|
"garage_version": "git:v0.8.0",
|
||||||
|
"knownNodes": {
|
||||||
|
"ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": {
|
||||||
|
"addr": "10.0.0.11:3901",
|
||||||
|
"is_up": true,
|
||||||
|
"last_seen_secs_ago": 9,
|
||||||
|
"hostname": "node1"
|
||||||
|
},
|
||||||
|
"4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": {
|
||||||
|
"addr": "10.0.0.12:3901",
|
||||||
|
"is_up": true,
|
||||||
|
"last_seen_secs_ago": 1,
|
||||||
|
"hostname": "node2"
|
||||||
|
},
|
||||||
|
"23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": {
|
||||||
|
"addr": "10.0.0.21:3901",
|
||||||
|
"is_up": true,
|
||||||
|
"last_seen_secs_ago": 7,
|
||||||
|
"hostname": "node3"
|
||||||
|
},
|
||||||
|
"e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": {
|
||||||
|
"addr": "10.0.0.22:3901",
|
||||||
|
"is_up": true,
|
||||||
|
"last_seen_secs_ago": 1,
|
||||||
|
"hostname": "node4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"version": 12,
|
||||||
|
"roles": {
|
||||||
|
"ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": {
|
||||||
|
"zone": "dc1",
|
||||||
|
"capacity": 4,
|
||||||
|
"tags": [
|
||||||
|
"node1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": {
|
||||||
|
"zone": "dc1",
|
||||||
|
"capacity": 6,
|
||||||
|
"tags": [
|
||||||
|
"node2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": {
|
||||||
|
"zone": "dc2",
|
||||||
|
"capacity": 10,
|
||||||
|
"tags": [
|
||||||
|
"node3"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stagedRoleChanges": {
|
||||||
|
"e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": {
|
||||||
|
"zone": "dc2",
|
||||||
|
"capacity": 5,
|
||||||
|
"tags": [
|
||||||
|
"node4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ConnectClusterNodes `POST /v0/connect`
|
||||||
|
|
||||||
|
Instructs this Garage node to connect to other Garage nodes at specified addresses.
|
||||||
|
|
||||||
|
Example request body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f@10.0.0.11:3901",
|
||||||
|
"4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff@10.0.0.12:3901"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The format of the string for a node to connect to is: `<node ID>@<ip address>:<port>`, same as in the `garage node connect` CLI call.
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"error": null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Handshake error",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GetClusterLayout `GET /v0/layout`
|
||||||
|
|
||||||
|
Returns the cluster's current layout in JSON, including:
|
||||||
|
|
||||||
|
- Currently configured cluster layout
|
||||||
|
- Staged changes to the cluster layout
|
||||||
|
|
||||||
|
(the info returned by this endpoint is a subset of the info returned by GetClusterStatus)
|
||||||
|
|
||||||
|
Example response body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 12,
|
||||||
|
"roles": {
|
||||||
|
"ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": {
|
||||||
|
"zone": "dc1",
|
||||||
|
"capacity": 4,
|
||||||
|
"tags": [
|
||||||
|
"node1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": {
|
||||||
|
"zone": "dc1",
|
||||||
|
"capacity": 6,
|
||||||
|
"tags": [
|
||||||
|
"node2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": {
|
||||||
|
"zone": "dc2",
|
||||||
|
"capacity": 10,
|
||||||
|
"tags": [
|
||||||
|
"node3"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stagedRoleChanges": {
|
||||||
|
"e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": {
|
||||||
|
"zone": "dc2",
|
||||||
|
"capacity": 5,
|
||||||
|
"tags": [
|
||||||
|
"node4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UpdateClusterLayout `POST /v0/layout`
|
||||||
|
|
||||||
|
Send modifications to the cluster layout. These modifications will
|
||||||
|
be included in the staged role changes, visible in subsequent calls
|
||||||
|
of `GetClusterLayout`. Once the set of staged changes is satisfactory,
|
||||||
|
the user may call `ApplyClusterLayout` to apply the changed changes,
|
||||||
|
or `Revert ClusterLayout` to clear all of the staged changes in
|
||||||
|
the layout.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
<node_id>: {
|
||||||
|
"capacity": <new_capacity>,
|
||||||
|
"zone": <new_zone>,
|
||||||
|
"tags": [
|
||||||
|
<new_tag>,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
<node_id_to_remove>: null,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Contrary to the CLI that may update only a subset of the fields
|
||||||
|
`capacity`, `zone` and `tags`, when calling this API all of these
|
||||||
|
values must be specified.
|
||||||
|
|
||||||
|
|
||||||
|
#### ApplyClusterLayout `POST /v0/layout/apply`
|
||||||
|
|
||||||
|
Applies to the cluster the layout changes currently registered as
|
||||||
|
staged layout changes.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 13
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly to the CLI, the body must include the version of the new layout
|
||||||
|
that will be created, which MUST be 1 + the value of the currently
|
||||||
|
existing layout in the cluster.
|
||||||
|
|
||||||
|
#### RevertClusterLayout `POST /v0/layout/revert`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
### Access key operations
|
||||||
|
|
||||||
|
#### ListKeys `GET /v0/key`
|
||||||
|
|
||||||
|
Returns all API access keys in the cluster.
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"name": "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GKe10061ac9c2921f09e4c5540",
|
||||||
|
"name": "test2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CreateKey `POST /v0/key`
|
||||||
|
|
||||||
|
Creates a new API access key.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "NameOfMyKey"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ImportKey `POST /v0/key/import`
|
||||||
|
|
||||||
|
Imports an existing API key.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"secretAccessKey": "b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835",
|
||||||
|
"name": "NameOfMyKey"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GetKeyInfo `GET /v0/key?id=<acces key id>`
|
||||||
|
#### GetKeyInfo `GET /v0/key?search=<pattern>`
|
||||||
|
|
||||||
|
Returns information about the requested API access key.
|
||||||
|
|
||||||
|
If `id` is set, the key is looked up using its exact identifier (faster).
|
||||||
|
If `search` is set, the key is looked up using its name or prefix
|
||||||
|
of identifier (slower, all keys are enumerated to do this).
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"secretAccessKey": "b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835",
|
||||||
|
"permissions": {
|
||||||
|
"createBucket": false
|
||||||
|
},
|
||||||
|
"buckets": [
|
||||||
|
{
|
||||||
|
"id": "70dc3bed7fe83a75e46b66e7ddef7d56e65f3c02f9f80b6749fb97eccb5e1033",
|
||||||
|
"globalAliases": [
|
||||||
|
"test2"
|
||||||
|
],
|
||||||
|
"localAliases": [],
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d7452a935e663fc1914f3a5515163a6d3724010ce8dfd9e4743ca8be5974f995",
|
||||||
|
"globalAliases": [
|
||||||
|
"test3"
|
||||||
|
],
|
||||||
|
"localAliases": [],
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||||
|
"globalAliases": [],
|
||||||
|
"localAliases": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "96470e0df00ec28807138daf01915cfda2bee8eccc91dea9558c0b4855b5bf95",
|
||||||
|
"globalAliases": [
|
||||||
|
"alex"
|
||||||
|
],
|
||||||
|
"localAliases": [],
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DeleteKey `DELETE /v0/key?id=<acces key id>`
|
||||||
|
|
||||||
|
Deletes an API access key.
|
||||||
|
|
||||||
|
#### UpdateKey `POST /v0/key?id=<acces key id>`
|
||||||
|
|
||||||
|
Updates information about the specified API access key.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "NameOfMyKey",
|
||||||
|
"allow": {
|
||||||
|
"createBucket": true,
|
||||||
|
},
|
||||||
|
"deny": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All fields (`name`, `allow` and `deny`) are optionnal.
|
||||||
|
If they are present, the corresponding modifications are applied to the key, otherwise nothing is changed.
|
||||||
|
The possible flags in `allow` and `deny` are: `createBucket`.
|
||||||
|
|
||||||
|
|
||||||
|
### Bucket operations
|
||||||
|
|
||||||
|
#### ListBuckets `GET /v0/bucket`
|
||||||
|
|
||||||
|
Returns all storage buckets in the cluster.
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "70dc3bed7fe83a75e46b66e7ddef7d56e65f3c02f9f80b6749fb97eccb5e1033",
|
||||||
|
"globalAliases": [
|
||||||
|
"test2"
|
||||||
|
],
|
||||||
|
"localAliases": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "96470e0df00ec28807138daf01915cfda2bee8eccc91dea9558c0b4855b5bf95",
|
||||||
|
"globalAliases": [
|
||||||
|
"alex"
|
||||||
|
],
|
||||||
|
"localAliases": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d7452a935e663fc1914f3a5515163a6d3724010ce8dfd9e4743ca8be5974f995",
|
||||||
|
"globalAliases": [
|
||||||
|
"test3"
|
||||||
|
],
|
||||||
|
"localAliases": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||||
|
"globalAliases": [],
|
||||||
|
"localAliases": [
|
||||||
|
{
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"alias": "test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GetBucketInfo `GET /v0/bucket?id=<bucket id>`
|
||||||
|
#### GetBucketInfo `GET /v0/bucket?globalAlias=<alias>`
|
||||||
|
|
||||||
|
Returns information about the requested storage bucket.
|
||||||
|
|
||||||
|
If `id` is set, the bucket is looked up using its exact identifier.
|
||||||
|
If `globalAlias` is set, the bucket is looked up using its global alias.
|
||||||
|
(both are fast)
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||||
|
"globalAliases": [
|
||||||
|
"alex"
|
||||||
|
],
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"name": "alex",
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": true
|
||||||
|
},
|
||||||
|
"bucketLocalAliases": [
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CreateBucket `POST /v0/bucket`
|
||||||
|
|
||||||
|
Creates a new storage bucket.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"globalAlias": "NameOfMyBucket"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
OR
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"localAlias": {
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"alias": "NameOfMyBucket",
|
||||||
|
"allPermissions": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
OR
|
||||||
|
|
||||||
|
```json
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a new bucket, either with a global alias, a local one,
|
||||||
|
or no alias at all.
|
||||||
|
|
||||||
|
#### DeleteBucket `DELETE /v0/bucket?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!
|
||||||
|
|
||||||
|
#### PutBucketWebsite `PUT /v0/bucket/website?id=<bucket id>`
|
||||||
|
|
||||||
|
Sets the website configuration for a bucket (this also enables website access for this bucket).
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"indexDocument": "index.html",
|
||||||
|
"errorDocument": "404.html",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The field `errorDocument` is optional, if no error document is set a generic error message is displayed when errors happen.
|
||||||
|
|
||||||
|
|
||||||
|
#### DeleteBucketWebsite `DELETE /v0/bucket/website?id=<bucket id>`
|
||||||
|
|
||||||
|
Deletes the website configuration for a bucket (disables website access for this bucket).
|
||||||
|
|
||||||
|
|
||||||
|
### Operations on permissions for keys on buckets
|
||||||
|
|
||||||
|
#### BucketAllowKey `POST /v0/bucket/allow`
|
||||||
|
|
||||||
|
Allows a key to do read/write/owner operations on a bucket.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Flags in `permissions` which have the value `true` will be activated.
|
||||||
|
Other flags will remain unchanged.
|
||||||
|
|
||||||
|
#### BucketDenyKey `POST /v0/bucket/deny`
|
||||||
|
|
||||||
|
Denies a key from doing read/write/owner operations on a bucket.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"permissions": {
|
||||||
|
"read": false,
|
||||||
|
"write": false,
|
||||||
|
"owner": true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Flags in `permissions` which have the value `true` will be deactivated.
|
||||||
|
Other flags will remain unchanged.
|
||||||
|
|
||||||
|
|
||||||
|
### Operations on bucket aliases
|
||||||
|
|
||||||
|
#### GlobalAliasBucket `PUT /v0/bucket/alias/global?id=<bucket id>&alias=<global alias>`
|
||||||
|
|
||||||
|
Empty body. Creates a global alias for a bucket.
|
||||||
|
|
||||||
|
#### GlobalUnaliasBucket `DELETE /v0/bucket/alias/global?id=<bucket id>&alias=<global alias>`
|
||||||
|
|
||||||
|
Removes a global alias for a bucket.
|
||||||
|
|
||||||
|
#### LocalAliasBucket `PUT /v0/bucket/alias/local?id=<bucket id>&accessKeyId=<access key ID>&alias=<local alias>`
|
||||||
|
|
||||||
|
Empty body. Creates a local alias for a bucket in the namespace of a specific access key.
|
||||||
|
|
||||||
|
#### LocalUnaliasBucket `DELETE /v0/bucket/alias/local?id=<bucket id>&accessKeyId<access key ID>&alias=<local alias>`
|
||||||
|
|
||||||
|
Removes a local alias for a bucket in the namespace of a specific access key.
|
||||||
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "garage_admin"
|
|
||||||
version = "0.7.0"
|
|
||||||
authors = ["Maximilien Richer <code@mricher.fr>"]
|
|
||||||
edition = "2018"
|
|
||||||
license = "AGPL-3.0"
|
|
||||||
description = "Administration and metrics REST HTTP server for Garage"
|
|
||||||
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
path = "lib.rs"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
garage_util = { version = "0.7.0", path = "../util" }
|
|
||||||
|
|
||||||
hex = "0.4"
|
|
||||||
|
|
||||||
futures = "0.3"
|
|
||||||
futures-util = "0.3"
|
|
||||||
http = "0.2"
|
|
||||||
hyper = "0.14"
|
|
||||||
tracing = "0.1.30"
|
|
||||||
|
|
||||||
opentelemetry = { version = "0.17", features = [ "rt-tokio" ] }
|
|
||||||
opentelemetry-prometheus = "0.10"
|
|
||||||
opentelemetry-otlp = "0.10"
|
|
||||||
prometheus = "0.13"
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
//! Crate for handling the admin and metric HTTP APIs
|
|
||||||
#[macro_use]
|
|
||||||
extern crate tracing;
|
|
||||||
|
|
||||||
pub mod metrics;
|
|
||||||
pub mod tracing_setup;
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
use std::convert::Infallible;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
use futures::future::*;
|
|
||||||
use hyper::{
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
service::{make_service_fn, service_fn},
|
|
||||||
Body, Method, Request, Response, Server,
|
|
||||||
};
|
|
||||||
|
|
||||||
use opentelemetry::{
|
|
||||||
global,
|
|
||||||
metrics::{BoundCounter, BoundValueRecorder},
|
|
||||||
trace::{FutureExt, TraceContextExt, Tracer},
|
|
||||||
Context,
|
|
||||||
};
|
|
||||||
use opentelemetry_prometheus::PrometheusExporter;
|
|
||||||
|
|
||||||
use prometheus::{Encoder, TextEncoder};
|
|
||||||
|
|
||||||
use garage_util::error::Error as GarageError;
|
|
||||||
use garage_util::metrics::*;
|
|
||||||
|
|
||||||
// serve_req on metric endpoint
|
|
||||||
async fn serve_req(
|
|
||||||
req: Request<Body>,
|
|
||||||
admin_server: Arc<AdminServer>,
|
|
||||||
) -> Result<Response<Body>, hyper::Error> {
|
|
||||||
debug!("Receiving request at path {}", req.uri());
|
|
||||||
let request_start = SystemTime::now();
|
|
||||||
|
|
||||||
admin_server.metrics.http_counter.add(1);
|
|
||||||
|
|
||||||
let response = match (req.method(), req.uri().path()) {
|
|
||||||
(&Method::GET, "/metrics") => {
|
|
||||||
let mut buffer = vec![];
|
|
||||||
let encoder = TextEncoder::new();
|
|
||||||
|
|
||||||
let tracer = opentelemetry::global::tracer("garage");
|
|
||||||
let metric_families = tracer.in_span("admin/gather_metrics", |_| {
|
|
||||||
admin_server.exporter.registry().gather()
|
|
||||||
});
|
|
||||||
|
|
||||||
encoder.encode(&metric_families, &mut buffer).unwrap();
|
|
||||||
admin_server
|
|
||||||
.metrics
|
|
||||||
.http_body_gauge
|
|
||||||
.record(buffer.len() as u64);
|
|
||||||
|
|
||||||
Response::builder()
|
|
||||||
.status(200)
|
|
||||||
.header(CONTENT_TYPE, encoder.format_type())
|
|
||||||
.body(Body::from(buffer))
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
_ => Response::builder()
|
|
||||||
.status(404)
|
|
||||||
.body(Body::from("Not implemented"))
|
|
||||||
.unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
admin_server
|
|
||||||
.metrics
|
|
||||||
.http_req_histogram
|
|
||||||
.record(request_start.elapsed().map_or(0.0, |d| d.as_secs_f64()));
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminServer hold the admin server internal admin_server and the metric exporter
|
|
||||||
pub struct AdminServer {
|
|
||||||
exporter: PrometheusExporter,
|
|
||||||
metrics: AdminServerMetrics,
|
|
||||||
}
|
|
||||||
|
|
||||||
// GarageMetricadmin_server holds the metrics counter definition for Garage
|
|
||||||
// FIXME: we would rather have that split up among the different libraries?
|
|
||||||
struct AdminServerMetrics {
|
|
||||||
http_counter: BoundCounter<u64>,
|
|
||||||
http_body_gauge: BoundValueRecorder<u64>,
|
|
||||||
http_req_histogram: BoundValueRecorder<f64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AdminServer {
|
|
||||||
/// init initilialize the AdminServer and background metric server
|
|
||||||
pub fn init() -> AdminServer {
|
|
||||||
let exporter = opentelemetry_prometheus::exporter().init();
|
|
||||||
let meter = global::meter("garage/admin_server");
|
|
||||||
AdminServer {
|
|
||||||
exporter,
|
|
||||||
metrics: AdminServerMetrics {
|
|
||||||
http_counter: meter
|
|
||||||
.u64_counter("admin.http_requests_total")
|
|
||||||
.with_description("Total number of HTTP requests made.")
|
|
||||||
.init()
|
|
||||||
.bind(&[]),
|
|
||||||
http_body_gauge: meter
|
|
||||||
.u64_value_recorder("admin.http_response_size_bytes")
|
|
||||||
.with_description("The metrics HTTP response sizes in bytes.")
|
|
||||||
.init()
|
|
||||||
.bind(&[]),
|
|
||||||
http_req_histogram: meter
|
|
||||||
.f64_value_recorder("admin.http_request_duration_seconds")
|
|
||||||
.with_description("The HTTP request latencies in seconds.")
|
|
||||||
.init()
|
|
||||||
.bind(&[]),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// run execute the admin server on the designated HTTP port and listen for requests
|
|
||||||
pub async fn run(
|
|
||||||
self,
|
|
||||||
bind_addr: SocketAddr,
|
|
||||||
shutdown_signal: impl Future<Output = ()>,
|
|
||||||
) -> Result<(), GarageError> {
|
|
||||||
let admin_server = Arc::new(self);
|
|
||||||
// For every connection, we must make a `Service` to handle all
|
|
||||||
// incoming HTTP requests on said connection.
|
|
||||||
let make_svc = make_service_fn(move |_conn| {
|
|
||||||
let admin_server = admin_server.clone();
|
|
||||||
// This is the `Service` that will handle the connection.
|
|
||||||
// `service_fn` is a helper to convert a function that
|
|
||||||
// returns a Response into a `Service`.
|
|
||||||
async move {
|
|
||||||
Ok::<_, Infallible>(service_fn(move |req| {
|
|
||||||
let tracer = opentelemetry::global::tracer("garage");
|
|
||||||
let span = tracer
|
|
||||||
.span_builder("admin/request")
|
|
||||||
.with_trace_id(gen_trace_id())
|
|
||||||
.start(&tracer);
|
|
||||||
|
|
||||||
serve_req(req, admin_server.clone())
|
|
||||||
.with_context(Context::current_with_span(span))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let server = Server::bind(&bind_addr).serve(make_svc);
|
|
||||||
let graceful = server.with_graceful_shutdown(shutdown_signal);
|
|
||||||
info!("Admin server listening on http://{}", bind_addr);
|
|
||||||
|
|
||||||
graceful.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -54,6 +54,9 @@ quick-xml = { version = "0.21", features = [ "serialize" ] }
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
|
|
||||||
opentelemetry = "0.17"
|
opentelemetry = "0.17"
|
||||||
|
opentelemetry-prometheus = "0.10"
|
||||||
|
opentelemetry-otlp = "0.10"
|
||||||
|
prometheus = "0.13"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
k2v = [ "garage_util/k2v", "garage_model/k2v" ]
|
k2v = [ "garage_util/k2v", "garage_model/k2v" ]
|
||||||
|
|
|
||||||
199
src/api/admin/api_server.rs
Normal file
199
src/api/admin/api_server.rs
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use futures::future::Future;
|
||||||
|
use http::header::{
|
||||||
|
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW, CONTENT_TYPE,
|
||||||
|
};
|
||||||
|
use hyper::{Body, Request, Response};
|
||||||
|
|
||||||
|
use opentelemetry::trace::{SpanRef, Tracer};
|
||||||
|
use opentelemetry_prometheus::PrometheusExporter;
|
||||||
|
use prometheus::{Encoder, TextEncoder};
|
||||||
|
|
||||||
|
use garage_model::garage::Garage;
|
||||||
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
|
use crate::generic_server::*;
|
||||||
|
|
||||||
|
use crate::admin::bucket::*;
|
||||||
|
use crate::admin::cluster::*;
|
||||||
|
use crate::admin::error::*;
|
||||||
|
use crate::admin::key::*;
|
||||||
|
use crate::admin::router::{Authorization, Endpoint};
|
||||||
|
|
||||||
|
pub struct AdminApiServer {
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
exporter: PrometheusExporter,
|
||||||
|
metrics_token: Option<String>,
|
||||||
|
admin_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminApiServer {
|
||||||
|
pub fn new(garage: Arc<Garage>) -> Self {
|
||||||
|
let exporter = opentelemetry_prometheus::exporter().init();
|
||||||
|
let cfg = &garage.config.admin;
|
||||||
|
let metrics_token = cfg
|
||||||
|
.metrics_token
|
||||||
|
.as_ref()
|
||||||
|
.map(|tok| format!("Bearer {}", tok));
|
||||||
|
let admin_token = cfg
|
||||||
|
.admin_token
|
||||||
|
.as_ref()
|
||||||
|
.map(|tok| format!("Bearer {}", tok));
|
||||||
|
Self {
|
||||||
|
garage,
|
||||||
|
exporter,
|
||||||
|
metrics_token,
|
||||||
|
admin_token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(self, shutdown_signal: impl Future<Output = ()>) -> Result<(), GarageError> {
|
||||||
|
if let Some(bind_addr) = self.garage.config.admin.api_bind_addr {
|
||||||
|
let region = self.garage.config.s3_api.s3_region.clone();
|
||||||
|
ApiServer::new(region, self)
|
||||||
|
.run_server(bind_addr, shutdown_signal)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_options(&self, _req: &Request<Body>) -> Result<Response<Body>, Error> {
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(204)
|
||||||
|
.header(ALLOW, "OPTIONS, GET, POST")
|
||||||
|
.header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST")
|
||||||
|
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||||
|
.body(Body::empty())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_metrics(&self) -> Result<Response<Body>, Error> {
|
||||||
|
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(200)
|
||||||
|
.header(CONTENT_TYPE, encoder.format_type())
|
||||||
|
.body(Body::from(buffer))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ApiHandler for AdminApiServer {
|
||||||
|
const API_NAME: &'static str = "admin";
|
||||||
|
const API_NAME_DISPLAY: &'static str = "Admin";
|
||||||
|
|
||||||
|
type Endpoint = Endpoint;
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn parse_endpoint(&self, req: &Request<Body>) -> Result<Endpoint, Error> {
|
||||||
|
Endpoint::from_request(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
req: Request<Body>,
|
||||||
|
endpoint: Endpoint,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let expected_auth_header =
|
||||||
|
match endpoint.authorization_type() {
|
||||||
|
Authorization::MetricsToken => self.metrics_token.as_ref(),
|
||||||
|
Authorization::AdminToken => match &self.admin_token {
|
||||||
|
None => return Err(Error::forbidden(
|
||||||
|
"Admin token isn't configured, admin API access is disabled for security.",
|
||||||
|
)),
|
||||||
|
Some(t) => Some(t),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(h) = expected_auth_header {
|
||||||
|
match req.headers().get("Authorization") {
|
||||||
|
None => return Err(Error::forbidden("Authorization token must be provided")),
|
||||||
|
Some(v) => {
|
||||||
|
let authorized = v.to_str().map(|hv| hv.trim() == h).unwrap_or(false);
|
||||||
|
if !authorized {
|
||||||
|
return Err(Error::forbidden("Invalid authorization token provided"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match endpoint {
|
||||||
|
Endpoint::Options => self.handle_options(&req),
|
||||||
|
Endpoint::Metrics => self.handle_metrics(),
|
||||||
|
Endpoint::GetClusterStatus => handle_get_cluster_status(&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, req).await,
|
||||||
|
// Keys
|
||||||
|
Endpoint::ListKeys => handle_list_keys(&self.garage).await,
|
||||||
|
Endpoint::GetKeyInfo { id, search } => {
|
||||||
|
handle_get_key_info(&self.garage, id, search).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::PutBucketWebsite { id } => {
|
||||||
|
handle_put_bucket_website(&self.garage, id, req).await
|
||||||
|
}
|
||||||
|
Endpoint::DeleteBucketWebsite { id } => {
|
||||||
|
handle_delete_bucket_website(&self.garage, id).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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiEndpoint for Endpoint {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
Endpoint::name(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_span_attributes(&self, _span: SpanRef<'_>) {}
|
||||||
|
}
|
||||||
542
src/api/admin/bucket.rs
Normal file
542
src/api/admin/bucket.rs
Normal file
|
|
@ -0,0 +1,542 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use hyper::{Body, Request, Response, StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use garage_util::crdt::*;
|
||||||
|
use garage_util::data::*;
|
||||||
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
|
use garage_table::*;
|
||||||
|
|
||||||
|
use garage_model::bucket_alias_table::*;
|
||||||
|
use garage_model::bucket_table::*;
|
||||||
|
use garage_model::garage::Garage;
|
||||||
|
use garage_model::permission::*;
|
||||||
|
|
||||||
|
use crate::admin::error::*;
|
||||||
|
use crate::admin::key::ApiBucketKeyPerm;
|
||||||
|
use crate::common_error::CommonError;
|
||||||
|
use crate::helpers::parse_json_body;
|
||||||
|
|
||||||
|
pub async fn handle_list_buckets(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
||||||
|
let buckets = garage
|
||||||
|
.bucket_table
|
||||||
|
.get_range(
|
||||||
|
&EmptyKey,
|
||||||
|
None,
|
||||||
|
Some(DeletedFilter::NotDeleted),
|
||||||
|
10000,
|
||||||
|
EnumerationOrder::Forward,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let res = buckets
|
||||||
|
.into_iter()
|
||||||
|
.map(|b| {
|
||||||
|
let state = b.state.as_option().unwrap();
|
||||||
|
ListBucketResultItem {
|
||||||
|
id: hex::encode(b.id),
|
||||||
|
global_aliases: state
|
||||||
|
.aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, a)| *a)
|
||||||
|
.map(|(n, _, _)| n.to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
local_aliases: state
|
||||||
|
.local_aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, a)| *a)
|
||||||
|
.map(|((k, n), _, _)| BucketLocalAlias {
|
||||||
|
access_key_id: k.to_string(),
|
||||||
|
alias: n.to_string(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::from(resp_json))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct ListBucketResultItem {
|
||||||
|
id: String,
|
||||||
|
global_aliases: Vec<String>,
|
||||||
|
local_aliases: Vec<BucketLocalAlias>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct BucketLocalAlias {
|
||||||
|
access_key_id: String,
|
||||||
|
alias: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_get_bucket_info(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
id: Option<String>,
|
||||||
|
global_alias: Option<String>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let bucket_id = match (id, global_alias) {
|
||||||
|
(Some(id), None) => parse_bucket_id(&id)?,
|
||||||
|
(None, Some(ga)) => garage
|
||||||
|
.bucket_helper()
|
||||||
|
.resolve_global_bucket_name(&ga)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?,
|
||||||
|
_ => {
|
||||||
|
return Err(Error::bad_request(
|
||||||
|
"Either id or globalAlias must be provided (but not both)",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bucket_info_results(garage, bucket_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn bucket_info_results(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
bucket_id: Uuid,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let bucket = garage
|
||||||
|
.bucket_helper()
|
||||||
|
.get_existing_bucket(bucket_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut relevant_keys = HashMap::new();
|
||||||
|
for (k, _) in bucket
|
||||||
|
.state
|
||||||
|
.as_option()
|
||||||
|
.unwrap()
|
||||||
|
.authorized_keys
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
{
|
||||||
|
if let Some(key) = garage
|
||||||
|
.key_table
|
||||||
|
.get(&EmptyKey, k)
|
||||||
|
.await?
|
||||||
|
.filter(|k| !k.is_deleted())
|
||||||
|
{
|
||||||
|
if !key.state.is_deleted() {
|
||||||
|
relevant_keys.insert(k.clone(), key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ((k, _), _, _) in bucket
|
||||||
|
.state
|
||||||
|
.as_option()
|
||||||
|
.unwrap()
|
||||||
|
.local_aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
{
|
||||||
|
if relevant_keys.contains_key(k) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(key) = garage.key_table.get(&EmptyKey, k).await? {
|
||||||
|
if !key.state.is_deleted() {
|
||||||
|
relevant_keys.insert(k.clone(), key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = bucket.state.as_option().unwrap();
|
||||||
|
|
||||||
|
let res =
|
||||||
|
GetBucketInfoResult {
|
||||||
|
id: hex::encode(&bucket.id),
|
||||||
|
global_aliases: state
|
||||||
|
.aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, a)| *a)
|
||||||
|
.map(|(n, _, _)| n.to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
website_access: state.website_config.get().is_some(),
|
||||||
|
website_config: state.website_config.get().clone().map(|wsc| {
|
||||||
|
GetBucketInfoWebsiteResult {
|
||||||
|
index_document: wsc.index_document,
|
||||||
|
error_document: wsc.error_document,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
keys: relevant_keys
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, key)| {
|
||||||
|
let p = key.state.as_option().unwrap();
|
||||||
|
GetBucketInfoKey {
|
||||||
|
access_key_id: key.key_id,
|
||||||
|
name: p.name.get().to_string(),
|
||||||
|
permissions: p
|
||||||
|
.authorized_buckets
|
||||||
|
.get(&bucket.id)
|
||||||
|
.map(|p| ApiBucketKeyPerm {
|
||||||
|
read: p.allow_read,
|
||||||
|
write: p.allow_write,
|
||||||
|
owner: p.allow_owner,
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
bucket_local_aliases: p
|
||||||
|
.local_aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, b)| *b == Some(bucket.id))
|
||||||
|
.map(|(n, _, _)| n.to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::from(resp_json))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct GetBucketInfoResult {
|
||||||
|
id: String,
|
||||||
|
global_aliases: Vec<String>,
|
||||||
|
website_access: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
website_config: Option<GetBucketInfoWebsiteResult>,
|
||||||
|
keys: Vec<GetBucketInfoKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct GetBucketInfoWebsiteResult {
|
||||||
|
index_document: String,
|
||||||
|
error_document: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct GetBucketInfoKey {
|
||||||
|
access_key_id: String,
|
||||||
|
name: String,
|
||||||
|
permissions: ApiBucketKeyPerm,
|
||||||
|
bucket_local_aliases: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_create_bucket(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let req = parse_json_body::<CreateBucketRequest>(req).await?;
|
||||||
|
|
||||||
|
if let Some(ga) = &req.global_alias {
|
||||||
|
if !is_valid_bucket_name(ga) {
|
||||||
|
return Err(Error::bad_request(format!(
|
||||||
|
"{}: {}",
|
||||||
|
ga, INVALID_BUCKET_NAME_MESSAGE
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(alias) = garage.bucket_alias_table.get(&EmptyKey, ga).await? {
|
||||||
|
if alias.state.get().is_some() {
|
||||||
|
return Err(CommonError::BucketAlreadyExists.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(la) = &req.local_alias {
|
||||||
|
if !is_valid_bucket_name(&la.alias) {
|
||||||
|
return Err(Error::bad_request(format!(
|
||||||
|
"{}: {}",
|
||||||
|
la.alias, INVALID_BUCKET_NAME_MESSAGE
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = garage
|
||||||
|
.key_helper()
|
||||||
|
.get_existing_key(&la.access_key_id)
|
||||||
|
.await?;
|
||||||
|
let state = key.state.as_option().unwrap();
|
||||||
|
if matches!(state.local_aliases.get(&la.alias), Some(_)) {
|
||||||
|
return Err(Error::bad_request("Local alias already exists"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bucket = Bucket::new();
|
||||||
|
garage.bucket_table.insert(&bucket).await?;
|
||||||
|
|
||||||
|
if let Some(ga) = &req.global_alias {
|
||||||
|
garage
|
||||||
|
.bucket_helper()
|
||||||
|
.set_global_bucket_alias(bucket.id, ga)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(la) = &req.local_alias {
|
||||||
|
garage
|
||||||
|
.bucket_helper()
|
||||||
|
.set_local_bucket_alias(bucket.id, &la.access_key_id, &la.alias)
|
||||||
|
.await?;
|
||||||
|
if la.all_permissions {
|
||||||
|
garage
|
||||||
|
.bucket_helper()
|
||||||
|
.set_bucket_key_permissions(
|
||||||
|
bucket.id,
|
||||||
|
&la.access_key_id,
|
||||||
|
BucketKeyPerm::ALL_PERMISSIONS,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket_info_results(garage, bucket.id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct CreateBucketRequest {
|
||||||
|
global_alias: Option<String>,
|
||||||
|
local_alias: Option<CreateBucketLocalAlias>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct CreateBucketLocalAlias {
|
||||||
|
access_key_id: String,
|
||||||
|
alias: String,
|
||||||
|
#[serde(default)]
|
||||||
|
all_permissions: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_delete_bucket(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let helper = garage.bucket_helper();
|
||||||
|
|
||||||
|
let bucket_id = parse_bucket_id(&id)?;
|
||||||
|
|
||||||
|
let mut bucket = helper.get_existing_bucket(bucket_id).await?;
|
||||||
|
let state = bucket.state.as_option().unwrap();
|
||||||
|
|
||||||
|
// Check bucket is empty
|
||||||
|
if !helper.is_bucket_empty(bucket_id).await? {
|
||||||
|
return Err(CommonError::BucketNotEmpty.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- done checking, now commit ---
|
||||||
|
// 1. delete authorization from keys that had access
|
||||||
|
for (key_id, perm) in bucket.authorized_keys() {
|
||||||
|
if perm.is_any() {
|
||||||
|
helper
|
||||||
|
.set_bucket_key_permissions(bucket.id, key_id, BucketKeyPerm::NO_PERMISSIONS)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. delete all local aliases
|
||||||
|
for ((key_id, alias), _, active) in state.local_aliases.items().iter() {
|
||||||
|
if *active {
|
||||||
|
helper
|
||||||
|
.unset_local_bucket_alias(bucket.id, key_id, alias)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. delete all global aliases
|
||||||
|
for (alias, _, active) in state.aliases.items().iter() {
|
||||||
|
if *active {
|
||||||
|
helper.purge_global_bucket_alias(bucket.id, alias).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. delete bucket
|
||||||
|
bucket.state = Deletable::delete();
|
||||||
|
garage.bucket_table.insert(&bucket).await?;
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::NO_CONTENT)
|
||||||
|
.body(Body::empty())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- BUCKET WEBSITE CONFIGURATION ----
|
||||||
|
|
||||||
|
pub async fn handle_put_bucket_website(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
id: String,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let req = parse_json_body::<PutBucketWebsiteRequest>(req).await?;
|
||||||
|
let bucket_id = parse_bucket_id(&id)?;
|
||||||
|
|
||||||
|
let mut bucket = garage
|
||||||
|
.bucket_helper()
|
||||||
|
.get_existing_bucket(bucket_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let state = bucket.state.as_option_mut().unwrap();
|
||||||
|
state.website_config.update(Some(WebsiteConfig {
|
||||||
|
index_document: req.index_document,
|
||||||
|
error_document: req.error_document,
|
||||||
|
}));
|
||||||
|
|
||||||
|
garage.bucket_table.insert(&bucket).await?;
|
||||||
|
|
||||||
|
bucket_info_results(garage, bucket_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct PutBucketWebsiteRequest {
|
||||||
|
index_document: String,
|
||||||
|
#[serde(default)]
|
||||||
|
error_document: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_delete_bucket_website(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let bucket_id = parse_bucket_id(&id)?;
|
||||||
|
|
||||||
|
let mut bucket = garage
|
||||||
|
.bucket_helper()
|
||||||
|
.get_existing_bucket(bucket_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let state = bucket.state.as_option_mut().unwrap();
|
||||||
|
state.website_config.update(None);
|
||||||
|
|
||||||
|
garage.bucket_table.insert(&bucket).await?;
|
||||||
|
|
||||||
|
bucket_info_results(garage, bucket_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- BUCKET/KEY PERMISSIONS ----
|
||||||
|
|
||||||
|
pub async fn handle_bucket_change_key_perm(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
new_perm_flag: bool,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let req = parse_json_body::<BucketKeyPermChangeRequest>(req).await?;
|
||||||
|
|
||||||
|
let bucket_id = parse_bucket_id(&req.bucket_id)?;
|
||||||
|
|
||||||
|
let bucket = garage
|
||||||
|
.bucket_helper()
|
||||||
|
.get_existing_bucket(bucket_id)
|
||||||
|
.await?;
|
||||||
|
let state = bucket.state.as_option().unwrap();
|
||||||
|
|
||||||
|
let key = garage
|
||||||
|
.key_helper()
|
||||||
|
.get_existing_key(&req.access_key_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut perm = state
|
||||||
|
.authorized_keys
|
||||||
|
.get(&key.key_id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(BucketKeyPerm::NO_PERMISSIONS);
|
||||||
|
|
||||||
|
if req.permissions.read {
|
||||||
|
perm.allow_read = new_perm_flag;
|
||||||
|
}
|
||||||
|
if req.permissions.write {
|
||||||
|
perm.allow_write = new_perm_flag;
|
||||||
|
}
|
||||||
|
if req.permissions.owner {
|
||||||
|
perm.allow_owner = new_perm_flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
garage
|
||||||
|
.bucket_helper()
|
||||||
|
.set_bucket_key_permissions(bucket.id, &key.key_id, perm)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
bucket_info_results(garage, bucket.id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct BucketKeyPermChangeRequest {
|
||||||
|
bucket_id: String,
|
||||||
|
access_key_id: String,
|
||||||
|
permissions: ApiBucketKeyPerm,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- BUCKET ALIASES ----
|
||||||
|
|
||||||
|
pub async fn handle_global_alias_bucket(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
bucket_id: String,
|
||||||
|
alias: String,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let bucket_id = parse_bucket_id(&bucket_id)?;
|
||||||
|
|
||||||
|
garage
|
||||||
|
.bucket_helper()
|
||||||
|
.set_global_bucket_alias(bucket_id, &alias)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
bucket_info_results(garage, bucket_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_global_unalias_bucket(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
bucket_id: String,
|
||||||
|
alias: String,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let bucket_id = parse_bucket_id(&bucket_id)?;
|
||||||
|
|
||||||
|
garage
|
||||||
|
.bucket_helper()
|
||||||
|
.unset_global_bucket_alias(bucket_id, &alias)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
bucket_info_results(garage, bucket_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_local_alias_bucket(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
bucket_id: String,
|
||||||
|
access_key_id: String,
|
||||||
|
alias: String,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let bucket_id = parse_bucket_id(&bucket_id)?;
|
||||||
|
|
||||||
|
garage
|
||||||
|
.bucket_helper()
|
||||||
|
.set_local_bucket_alias(bucket_id, &access_key_id, &alias)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
bucket_info_results(garage, bucket_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_local_unalias_bucket(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
bucket_id: String,
|
||||||
|
access_key_id: String,
|
||||||
|
alias: String,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let bucket_id = parse_bucket_id(&bucket_id)?;
|
||||||
|
|
||||||
|
garage
|
||||||
|
.bucket_helper()
|
||||||
|
.unset_local_bucket_alias(bucket_id, &access_key_id, &alias)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
bucket_info_results(garage, bucket_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- HELPER ----
|
||||||
|
|
||||||
|
fn parse_bucket_id(id: &str) -> Result<Uuid, Error> {
|
||||||
|
let id_hex = hex::decode(&id).ok_or_bad_request("Invalid bucket id")?;
|
||||||
|
Ok(Uuid::try_from(&id_hex).ok_or_bad_request("Invalid bucket id")?)
|
||||||
|
}
|
||||||
198
src/api/admin/cluster.rs
Normal file
198
src/api/admin/cluster.rs
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use hyper::{Body, Request, Response, StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use garage_util::crdt::*;
|
||||||
|
use garage_util::data::*;
|
||||||
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
|
use garage_rpc::layout::*;
|
||||||
|
|
||||||
|
use garage_model::garage::Garage;
|
||||||
|
|
||||||
|
use crate::admin::error::*;
|
||||||
|
use crate::helpers::parse_json_body;
|
||||||
|
|
||||||
|
pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
||||||
|
let res = GetClusterStatusResponse {
|
||||||
|
node: hex::encode(garage.system.id),
|
||||||
|
garage_version: garage.system.garage_version(),
|
||||||
|
known_nodes: garage
|
||||||
|
.system
|
||||||
|
.get_known_nodes()
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| {
|
||||||
|
(
|
||||||
|
hex::encode(i.id),
|
||||||
|
KnownNodeResp {
|
||||||
|
addr: i.addr,
|
||||||
|
is_up: i.is_up,
|
||||||
|
last_seen_secs_ago: i.last_seen_secs_ago,
|
||||||
|
hostname: i.status.hostname,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
layout: get_cluster_layout(garage),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::from(resp_json))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_connect_cluster_nodes(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let req = parse_json_body::<Vec<String>>(req).await?;
|
||||||
|
|
||||||
|
let res = futures::future::join_all(req.iter().map(|node| garage.system.connect(node)))
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| match r {
|
||||||
|
Ok(()) => ConnectClusterNodesResponse {
|
||||||
|
success: true,
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(e) => ConnectClusterNodesResponse {
|
||||||
|
success: false,
|
||||||
|
error: Some(format!("{}", e)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::from(resp_json))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_get_cluster_layout(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
||||||
|
let res = get_cluster_layout(garage);
|
||||||
|
let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::from(resp_json))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cluster_layout(garage: &Arc<Garage>) -> GetClusterLayoutResponse {
|
||||||
|
let layout = garage.system.get_cluster_layout();
|
||||||
|
|
||||||
|
GetClusterLayoutResponse {
|
||||||
|
version: layout.version,
|
||||||
|
roles: layout
|
||||||
|
.roles
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, v)| v.0.is_some())
|
||||||
|
.map(|(k, _, v)| (hex::encode(k), v.0.clone()))
|
||||||
|
.collect(),
|
||||||
|
staged_role_changes: layout
|
||||||
|
.staging
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, _, v)| layout.roles.get(k) != Some(v))
|
||||||
|
.map(|(k, _, v)| (hex::encode(k), v.0.clone()))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct GetClusterStatusResponse {
|
||||||
|
node: String,
|
||||||
|
garage_version: &'static str,
|
||||||
|
known_nodes: HashMap<String, KnownNodeResp>,
|
||||||
|
layout: GetClusterLayoutResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ConnectClusterNodesResponse {
|
||||||
|
success: bool,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct GetClusterLayoutResponse {
|
||||||
|
version: u64,
|
||||||
|
roles: HashMap<String, Option<NodeRole>>,
|
||||||
|
staged_role_changes: HashMap<String, Option<NodeRole>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct KnownNodeResp {
|
||||||
|
addr: SocketAddr,
|
||||||
|
is_up: bool,
|
||||||
|
last_seen_secs_ago: Option<u64>,
|
||||||
|
hostname: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_update_cluster_layout(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let updates = parse_json_body::<UpdateClusterLayoutRequest>(req).await?;
|
||||||
|
|
||||||
|
let mut layout = garage.system.get_cluster_layout();
|
||||||
|
|
||||||
|
let mut roles = layout.roles.clone();
|
||||||
|
roles.merge(&layout.staging);
|
||||||
|
|
||||||
|
for (node, role) in updates {
|
||||||
|
let node = hex::decode(node).ok_or_bad_request("Invalid node identifier")?;
|
||||||
|
let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?;
|
||||||
|
|
||||||
|
layout
|
||||||
|
.staging
|
||||||
|
.merge(&roles.update_mutator(node, NodeRoleV(role)));
|
||||||
|
}
|
||||||
|
|
||||||
|
garage.system.update_cluster_layout(&layout).await?;
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::empty())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_apply_cluster_layout(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let param = parse_json_body::<ApplyRevertLayoutRequest>(req).await?;
|
||||||
|
|
||||||
|
let layout = garage.system.get_cluster_layout();
|
||||||
|
let layout = layout.apply_staged_changes(Some(param.version))?;
|
||||||
|
garage.system.update_cluster_layout(&layout).await?;
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::empty())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_revert_cluster_layout(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let param = parse_json_body::<ApplyRevertLayoutRequest>(req).await?;
|
||||||
|
|
||||||
|
let layout = garage.system.get_cluster_layout();
|
||||||
|
let layout = layout.revert_staged_changes(Some(param.version))?;
|
||||||
|
garage.system.update_cluster_layout(&layout).await?;
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::empty())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateClusterLayoutRequest = HashMap<String, Option<NodeRole>>;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ApplyRevertLayoutRequest {
|
||||||
|
version: u64,
|
||||||
|
}
|
||||||
96
src/api/admin/error.rs
Normal file
96
src/api/admin/error.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
use err_derive::Error;
|
||||||
|
use hyper::header::HeaderValue;
|
||||||
|
use hyper::{Body, HeaderMap, StatusCode};
|
||||||
|
|
||||||
|
pub use garage_model::helper::error::Error as HelperError;
|
||||||
|
|
||||||
|
use crate::common_error::CommonError;
|
||||||
|
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
||||||
|
use crate::generic_server::ApiError;
|
||||||
|
use crate::helpers::CustomApiErrorBody;
|
||||||
|
|
||||||
|
/// Errors of this crate
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(display = "{}", _0)]
|
||||||
|
/// Error from common error
|
||||||
|
Common(CommonError),
|
||||||
|
|
||||||
|
// Category: cannot process
|
||||||
|
/// The API access key does not exist
|
||||||
|
#[error(display = "Access key not found: {}", _0)]
|
||||||
|
NoSuchAccessKey(String),
|
||||||
|
|
||||||
|
/// In Import key, the key already exists
|
||||||
|
#[error(
|
||||||
|
display = "Key {} already exists in data store. Even if it is deleted, we can't let you create a new key with the same ID. Sorry.",
|
||||||
|
_0
|
||||||
|
)]
|
||||||
|
KeyAlreadyExists(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Error
|
||||||
|
where
|
||||||
|
CommonError: From<T>,
|
||||||
|
{
|
||||||
|
fn from(err: T) -> Self {
|
||||||
|
Error::Common(CommonError::from(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommonErrorDerivative for Error {}
|
||||||
|
|
||||||
|
impl From<HelperError> for Error {
|
||||||
|
fn from(err: HelperError) -> Self {
|
||||||
|
match err {
|
||||||
|
HelperError::Internal(i) => Self::Common(CommonError::InternalError(i)),
|
||||||
|
HelperError::BadRequest(b) => Self::Common(CommonError::BadRequest(b)),
|
||||||
|
HelperError::InvalidBucketName(n) => Self::Common(CommonError::InvalidBucketName(n)),
|
||||||
|
HelperError::NoSuchBucket(n) => Self::Common(CommonError::NoSuchBucket(n)),
|
||||||
|
HelperError::NoSuchAccessKey(n) => Self::NoSuchAccessKey(n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
fn code(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Error::Common(c) => c.aws_code(),
|
||||||
|
Error::NoSuchAccessKey(_) => "NoSuchAccessKey",
|
||||||
|
Error::KeyAlreadyExists(_) => "KeyAlreadyExists",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiError for Error {
|
||||||
|
/// Get the HTTP status code that best represents the meaning of the error for the client
|
||||||
|
fn http_status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
Error::Common(c) => c.http_status_code(),
|
||||||
|
Error::NoSuchAccessKey(_) => StatusCode::NOT_FOUND,
|
||||||
|
Error::KeyAlreadyExists(_) => StatusCode::CONFLICT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_http_headers(&self, _header_map: &mut HeaderMap<HeaderValue>) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
fn http_body(&self, garage_region: &str, path: &str) -> Body {
|
||||||
|
let error = CustomApiErrorBody {
|
||||||
|
code: self.code().to_string(),
|
||||||
|
message: format!("{}", self),
|
||||||
|
path: path.to_string(),
|
||||||
|
region: garage_region.to_string(),
|
||||||
|
};
|
||||||
|
Body::from(serde_json::to_string_pretty(&error).unwrap_or_else(|_| {
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"code": "InternalError",
|
||||||
|
"message": "JSON encoding of error failed"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.into()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
264
src/api/admin/key.rs
Normal file
264
src/api/admin/key.rs
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use hyper::{Body, Request, Response, StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
|
use garage_table::*;
|
||||||
|
|
||||||
|
use garage_model::garage::Garage;
|
||||||
|
use garage_model::key_table::*;
|
||||||
|
|
||||||
|
use crate::admin::error::*;
|
||||||
|
use crate::helpers::parse_json_body;
|
||||||
|
|
||||||
|
pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
||||||
|
let res = garage
|
||||||
|
.key_table
|
||||||
|
.get_range(
|
||||||
|
&EmptyKey,
|
||||||
|
None,
|
||||||
|
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
|
||||||
|
10000,
|
||||||
|
EnumerationOrder::Forward,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|k| ListKeyResultItem {
|
||||||
|
id: k.key_id.to_string(),
|
||||||
|
name: k.params().unwrap().name.get().clone(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::from(resp_json))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ListKeyResultItem {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_get_key_info(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
id: Option<String>,
|
||||||
|
search: Option<String>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let key = if let Some(id) = id {
|
||||||
|
garage.key_helper().get_existing_key(&id).await?
|
||||||
|
} else if let Some(search) = search {
|
||||||
|
garage
|
||||||
|
.key_helper()
|
||||||
|
.get_existing_matching_key(&search)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
|
||||||
|
key_info_results(garage, key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_create_key(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let req = parse_json_body::<CreateKeyRequest>(req).await?;
|
||||||
|
|
||||||
|
let key = Key::new(&req.name);
|
||||||
|
garage.key_table.insert(&key).await?;
|
||||||
|
|
||||||
|
key_info_results(garage, key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CreateKeyRequest {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_import_key(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let req = parse_json_body::<ImportKeyRequest>(req).await?;
|
||||||
|
|
||||||
|
let prev_key = garage.key_table.get(&EmptyKey, &req.access_key_id).await?;
|
||||||
|
if prev_key.is_some() {
|
||||||
|
return Err(Error::KeyAlreadyExists(req.access_key_id.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let imported_key = Key::import(&req.access_key_id, &req.secret_access_key, &req.name);
|
||||||
|
garage.key_table.insert(&imported_key).await?;
|
||||||
|
|
||||||
|
key_info_results(garage, imported_key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct ImportKeyRequest {
|
||||||
|
access_key_id: String,
|
||||||
|
secret_access_key: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_update_key(
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
id: String,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let req = parse_json_body::<UpdateKeyRequest>(req).await?;
|
||||||
|
|
||||||
|
let mut key = garage.key_helper().get_existing_key(&id).await?;
|
||||||
|
|
||||||
|
let key_state = key.state.as_option_mut().unwrap();
|
||||||
|
|
||||||
|
if let Some(new_name) = req.name {
|
||||||
|
key_state.name.update(new_name);
|
||||||
|
}
|
||||||
|
if let Some(allow) = req.allow {
|
||||||
|
if allow.create_bucket {
|
||||||
|
key_state.allow_create_bucket.update(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(deny) = req.deny {
|
||||||
|
if deny.create_bucket {
|
||||||
|
key_state.allow_create_bucket.update(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
garage.key_table.insert(&key).await?;
|
||||||
|
|
||||||
|
key_info_results(garage, key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct UpdateKeyRequest {
|
||||||
|
name: Option<String>,
|
||||||
|
allow: Option<KeyPerm>,
|
||||||
|
deny: Option<KeyPerm>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_delete_key(garage: &Arc<Garage>, id: String) -> Result<Response<Body>, Error> {
|
||||||
|
let mut key = garage.key_helper().get_existing_key(&id).await?;
|
||||||
|
|
||||||
|
key.state.as_option().unwrap();
|
||||||
|
|
||||||
|
garage.key_helper().delete_key(&mut key).await?;
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::NO_CONTENT)
|
||||||
|
.body(Body::empty())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Body>, Error> {
|
||||||
|
let mut relevant_buckets = HashMap::new();
|
||||||
|
|
||||||
|
let key_state = key.state.as_option().unwrap();
|
||||||
|
|
||||||
|
for id in key_state
|
||||||
|
.authorized_buckets
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.map(|(id, _)| id)
|
||||||
|
.chain(
|
||||||
|
key_state
|
||||||
|
.local_aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(_, _, v)| v.as_ref()),
|
||||||
|
) {
|
||||||
|
if !relevant_buckets.contains_key(id) {
|
||||||
|
if let Some(b) = garage.bucket_table.get(&EmptyKey, id).await? {
|
||||||
|
if b.state.as_option().is_some() {
|
||||||
|
relevant_buckets.insert(*id, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = GetKeyInfoResult {
|
||||||
|
name: key_state.name.get().clone(),
|
||||||
|
access_key_id: key.key_id.clone(),
|
||||||
|
secret_access_key: key_state.secret_key.clone(),
|
||||||
|
permissions: KeyPerm {
|
||||||
|
create_bucket: *key_state.allow_create_bucket.get(),
|
||||||
|
},
|
||||||
|
buckets: relevant_buckets
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, bucket)| {
|
||||||
|
let state = bucket.state.as_option().unwrap();
|
||||||
|
KeyInfoBucketResult {
|
||||||
|
id: hex::encode(bucket.id),
|
||||||
|
global_aliases: state
|
||||||
|
.aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, a)| *a)
|
||||||
|
.map(|(n, _, _)| n.to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
local_aliases: state
|
||||||
|
.local_aliases
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|((k, _), _, a)| *a && *k == key.key_id)
|
||||||
|
.map(|((_, n), _, _)| n.to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
permissions: key_state
|
||||||
|
.authorized_buckets
|
||||||
|
.get(&bucket.id)
|
||||||
|
.map(|p| ApiBucketKeyPerm {
|
||||||
|
read: p.allow_read,
|
||||||
|
write: p.allow_write,
|
||||||
|
owner: p.allow_owner,
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::from(resp_json))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct GetKeyInfoResult {
|
||||||
|
name: String,
|
||||||
|
access_key_id: String,
|
||||||
|
secret_access_key: String,
|
||||||
|
permissions: KeyPerm,
|
||||||
|
buckets: Vec<KeyInfoBucketResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct KeyPerm {
|
||||||
|
#[serde(default)]
|
||||||
|
create_bucket: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct KeyInfoBucketResult {
|
||||||
|
id: String,
|
||||||
|
global_aliases: Vec<String>,
|
||||||
|
local_aliases: Vec<String>,
|
||||||
|
permissions: ApiBucketKeyPerm,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub(crate) struct ApiBucketKeyPerm {
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) read: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) write: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) owner: bool,
|
||||||
|
}
|
||||||
7
src/api/admin/mod.rs
Normal file
7
src/api/admin/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
pub mod api_server;
|
||||||
|
mod error;
|
||||||
|
mod router;
|
||||||
|
|
||||||
|
mod bucket;
|
||||||
|
mod cluster;
|
||||||
|
mod key;
|
||||||
149
src/api/admin/router.rs
Normal file
149
src/api/admin/router.rs
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use hyper::{Method, Request};
|
||||||
|
|
||||||
|
use crate::admin::error::*;
|
||||||
|
use crate::router_macros::*;
|
||||||
|
|
||||||
|
pub enum Authorization {
|
||||||
|
MetricsToken,
|
||||||
|
AdminToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
router_match! {@func
|
||||||
|
|
||||||
|
/// List of all Admin API endpoints.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Endpoint {
|
||||||
|
Options,
|
||||||
|
Metrics,
|
||||||
|
GetClusterStatus,
|
||||||
|
ConnectClusterNodes,
|
||||||
|
// Layout
|
||||||
|
GetClusterLayout,
|
||||||
|
UpdateClusterLayout,
|
||||||
|
ApplyClusterLayout,
|
||||||
|
RevertClusterLayout,
|
||||||
|
// Keys
|
||||||
|
ListKeys,
|
||||||
|
CreateKey,
|
||||||
|
ImportKey,
|
||||||
|
GetKeyInfo {
|
||||||
|
id: Option<String>,
|
||||||
|
search: Option<String>,
|
||||||
|
},
|
||||||
|
DeleteKey {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
UpdateKey {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
// Buckets
|
||||||
|
ListBuckets,
|
||||||
|
CreateBucket,
|
||||||
|
GetBucketInfo {
|
||||||
|
id: Option<String>,
|
||||||
|
global_alias: Option<String>,
|
||||||
|
},
|
||||||
|
DeleteBucket {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
PutBucketWebsite {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
DeleteBucketWebsite {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
// Bucket-Key Permissions
|
||||||
|
BucketAllowKey,
|
||||||
|
BucketDenyKey,
|
||||||
|
// Bucket aliases
|
||||||
|
GlobalAliasBucket {
|
||||||
|
id: String,
|
||||||
|
alias: String,
|
||||||
|
},
|
||||||
|
GlobalUnaliasBucket {
|
||||||
|
id: String,
|
||||||
|
alias: String,
|
||||||
|
},
|
||||||
|
LocalAliasBucket {
|
||||||
|
id: String,
|
||||||
|
access_key_id: String,
|
||||||
|
alias: String,
|
||||||
|
},
|
||||||
|
LocalUnaliasBucket {
|
||||||
|
id: String,
|
||||||
|
access_key_id: String,
|
||||||
|
alias: String,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
impl Endpoint {
|
||||||
|
/// Determine which S3 endpoint a request is for using the request, and a bucket which was
|
||||||
|
/// possibly extracted from the Host header.
|
||||||
|
/// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets
|
||||||
|
pub fn from_request<T>(req: &Request<T>) -> Result<Self, Error> {
|
||||||
|
let uri = req.uri();
|
||||||
|
let path = uri.path();
|
||||||
|
let query = uri.query();
|
||||||
|
|
||||||
|
let mut query = QueryParameters::from_query(query.unwrap_or_default())?;
|
||||||
|
|
||||||
|
let res = router_match!(@gen_path_parser (req.method(), path, query) [
|
||||||
|
OPTIONS _ => Options,
|
||||||
|
GET "/metrics" => Metrics,
|
||||||
|
GET "/v0/status" => GetClusterStatus,
|
||||||
|
POST "/v0/connect" => ConnectClusterNodes,
|
||||||
|
// Layout endpoints
|
||||||
|
GET "/v0/layout" => GetClusterLayout,
|
||||||
|
POST "/v0/layout" => UpdateClusterLayout,
|
||||||
|
POST "/v0/layout/apply" => ApplyClusterLayout,
|
||||||
|
POST "/v0/layout/revert" => RevertClusterLayout,
|
||||||
|
// API key endpoints
|
||||||
|
GET "/v0/key" if id => GetKeyInfo (query_opt::id, query_opt::search),
|
||||||
|
GET "/v0/key" if search => GetKeyInfo (query_opt::id, query_opt::search),
|
||||||
|
POST "/v0/key" if id => UpdateKey (query::id),
|
||||||
|
POST "/v0/key" => CreateKey,
|
||||||
|
POST "/v0/key/import" => ImportKey,
|
||||||
|
DELETE "/v0/key" if id => DeleteKey (query::id),
|
||||||
|
GET "/v0/key" => ListKeys,
|
||||||
|
// Bucket endpoints
|
||||||
|
GET "/v0/bucket" if id => GetBucketInfo (query_opt::id, query_opt::global_alias),
|
||||||
|
GET "/v0/bucket" if global_alias => GetBucketInfo (query_opt::id, query_opt::global_alias),
|
||||||
|
GET "/v0/bucket" => ListBuckets,
|
||||||
|
POST "/v0/bucket" => CreateBucket,
|
||||||
|
DELETE "/v0/bucket" if id => DeleteBucket (query::id),
|
||||||
|
PUT "/v0/bucket/website" if id => PutBucketWebsite (query::id),
|
||||||
|
DELETE "/v0/bucket/website" if id => DeleteBucketWebsite (query::id),
|
||||||
|
// Bucket-key permissions
|
||||||
|
POST "/v0/bucket/allow" => BucketAllowKey,
|
||||||
|
POST "/v0/bucket/deny" => BucketDenyKey,
|
||||||
|
// Bucket aliases
|
||||||
|
PUT "/v0/bucket/alias/global" => GlobalAliasBucket (query::id, query::alias),
|
||||||
|
DELETE "/v0/bucket/alias/global" => GlobalUnaliasBucket (query::id, query::alias),
|
||||||
|
PUT "/v0/bucket/alias/local" => LocalAliasBucket (query::id, query::access_key_id, query::alias),
|
||||||
|
DELETE "/v0/bucket/alias/local" => LocalUnaliasBucket (query::id, query::access_key_id, query::alias),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if let Some(message) = query.nonempty_message() {
|
||||||
|
debug!("Unused query parameter: {}", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
/// Get the kind of authorization which is required to perform the operation.
|
||||||
|
pub fn authorization_type(&self) -> Authorization {
|
||||||
|
match self {
|
||||||
|
Self::Metrics => Authorization::MetricsToken,
|
||||||
|
_ => Authorization::AdminToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateQueryParameters! {
|
||||||
|
"id" => id,
|
||||||
|
"search" => search,
|
||||||
|
"globalAlias" => global_alias,
|
||||||
|
"alias" => alias,
|
||||||
|
"accessKeyId" => access_key_id
|
||||||
|
}
|
||||||
177
src/api/common_error.rs
Normal file
177
src/api/common_error.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
use err_derive::Error;
|
||||||
|
use hyper::StatusCode;
|
||||||
|
|
||||||
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
|
/// Errors of this crate
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CommonError {
|
||||||
|
// ---- INTERNAL ERRORS ----
|
||||||
|
/// Error related to deeper parts of Garage
|
||||||
|
#[error(display = "Internal error: {}", _0)]
|
||||||
|
InternalError(#[error(source)] GarageError),
|
||||||
|
|
||||||
|
/// Error related to Hyper
|
||||||
|
#[error(display = "Internal error (Hyper error): {}", _0)]
|
||||||
|
Hyper(#[error(source)] hyper::Error),
|
||||||
|
|
||||||
|
/// Error related to HTTP
|
||||||
|
#[error(display = "Internal error (HTTP error): {}", _0)]
|
||||||
|
Http(#[error(source)] http::Error),
|
||||||
|
|
||||||
|
// ---- GENERIC CLIENT ERRORS ----
|
||||||
|
/// Proper authentication was not provided
|
||||||
|
#[error(display = "Forbidden: {}", _0)]
|
||||||
|
Forbidden(String),
|
||||||
|
|
||||||
|
/// Generic bad request response with custom message
|
||||||
|
#[error(display = "Bad request: {}", _0)]
|
||||||
|
BadRequest(String),
|
||||||
|
|
||||||
|
// ---- SPECIFIC ERROR CONDITIONS ----
|
||||||
|
// These have to be error codes referenced in the S3 spec here:
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
|
||||||
|
/// The bucket requested don't exists
|
||||||
|
#[error(display = "Bucket not found: {}", _0)]
|
||||||
|
NoSuchBucket(String),
|
||||||
|
|
||||||
|
/// Tried to create a bucket that already exist
|
||||||
|
#[error(display = "Bucket already exists")]
|
||||||
|
BucketAlreadyExists,
|
||||||
|
|
||||||
|
/// Tried to delete a non-empty bucket
|
||||||
|
#[error(display = "Tried to delete a non-empty bucket")]
|
||||||
|
BucketNotEmpty,
|
||||||
|
|
||||||
|
// Category: bad request
|
||||||
|
/// Bucket name is not valid according to AWS S3 specs
|
||||||
|
#[error(display = "Invalid bucket name: {}", _0)]
|
||||||
|
InvalidBucketName(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommonError {
|
||||||
|
pub fn http_status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
CommonError::InternalError(
|
||||||
|
GarageError::Timeout
|
||||||
|
| GarageError::RemoteError(_)
|
||||||
|
| GarageError::Quorum(_, _, _, _),
|
||||||
|
) => StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
CommonError::InternalError(_) | CommonError::Hyper(_) | CommonError::Http(_) => {
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
}
|
||||||
|
CommonError::BadRequest(_) => StatusCode::BAD_REQUEST,
|
||||||
|
CommonError::Forbidden(_) => StatusCode::FORBIDDEN,
|
||||||
|
CommonError::NoSuchBucket(_) => StatusCode::NOT_FOUND,
|
||||||
|
CommonError::BucketNotEmpty | CommonError::BucketAlreadyExists => StatusCode::CONFLICT,
|
||||||
|
CommonError::InvalidBucketName(_) => StatusCode::BAD_REQUEST,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn aws_code(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
CommonError::Forbidden(_) => "AccessDenied",
|
||||||
|
CommonError::InternalError(
|
||||||
|
GarageError::Timeout
|
||||||
|
| GarageError::RemoteError(_)
|
||||||
|
| GarageError::Quorum(_, _, _, _),
|
||||||
|
) => "ServiceUnavailable",
|
||||||
|
CommonError::InternalError(_) | CommonError::Hyper(_) | CommonError::Http(_) => {
|
||||||
|
"InternalError"
|
||||||
|
}
|
||||||
|
CommonError::BadRequest(_) => "InvalidRequest",
|
||||||
|
CommonError::NoSuchBucket(_) => "NoSuchBucket",
|
||||||
|
CommonError::BucketAlreadyExists => "BucketAlreadyExists",
|
||||||
|
CommonError::BucketNotEmpty => "BucketNotEmpty",
|
||||||
|
CommonError::InvalidBucketName(_) => "InvalidBucketName",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bad_request<M: ToString>(msg: M) -> Self {
|
||||||
|
CommonError::BadRequest(msg.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CommonErrorDerivative: From<CommonError> {
|
||||||
|
fn internal_error<M: ToString>(msg: M) -> Self {
|
||||||
|
Self::from(CommonError::InternalError(GarageError::Message(
|
||||||
|
msg.to_string(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bad_request<M: ToString>(msg: M) -> Self {
|
||||||
|
Self::from(CommonError::BadRequest(msg.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forbidden<M: ToString>(msg: M) -> Self {
|
||||||
|
Self::from(CommonError::Forbidden(msg.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait to map error to the Bad Request error code
|
||||||
|
pub trait OkOrBadRequest {
|
||||||
|
type S;
|
||||||
|
fn ok_or_bad_request<M: AsRef<str>>(self, reason: M) -> Result<Self::S, CommonError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> OkOrBadRequest for Result<T, E>
|
||||||
|
where
|
||||||
|
E: std::fmt::Display,
|
||||||
|
{
|
||||||
|
type S = T;
|
||||||
|
fn ok_or_bad_request<M: AsRef<str>>(self, reason: M) -> Result<T, CommonError> {
|
||||||
|
match self {
|
||||||
|
Ok(x) => Ok(x),
|
||||||
|
Err(e) => Err(CommonError::BadRequest(format!(
|
||||||
|
"{}: {}",
|
||||||
|
reason.as_ref(),
|
||||||
|
e
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> OkOrBadRequest for Option<T> {
|
||||||
|
type S = T;
|
||||||
|
fn ok_or_bad_request<M: AsRef<str>>(self, reason: M) -> Result<T, CommonError> {
|
||||||
|
match self {
|
||||||
|
Some(x) => Ok(x),
|
||||||
|
None => Err(CommonError::BadRequest(reason.as_ref().to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait to map an error to an Internal Error code
|
||||||
|
pub trait OkOrInternalError {
|
||||||
|
type S;
|
||||||
|
fn ok_or_internal_error<M: AsRef<str>>(self, reason: M) -> Result<Self::S, CommonError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> OkOrInternalError for Result<T, E>
|
||||||
|
where
|
||||||
|
E: std::fmt::Display,
|
||||||
|
{
|
||||||
|
type S = T;
|
||||||
|
fn ok_or_internal_error<M: AsRef<str>>(self, reason: M) -> Result<T, CommonError> {
|
||||||
|
match self {
|
||||||
|
Ok(x) => Ok(x),
|
||||||
|
Err(e) => Err(CommonError::InternalError(GarageError::Message(format!(
|
||||||
|
"{}: {}",
|
||||||
|
reason.as_ref(),
|
||||||
|
e
|
||||||
|
)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> OkOrInternalError for Option<T> {
|
||||||
|
type S = T;
|
||||||
|
fn ok_or_internal_error<M: AsRef<str>>(self, reason: M) -> Result<T, CommonError> {
|
||||||
|
match self {
|
||||||
|
Some(x) => Ok(x),
|
||||||
|
None => Err(CommonError::InternalError(GarageError::Message(
|
||||||
|
reason.as_ref().to_string(),
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,11 @@ use async_trait::async_trait;
|
||||||
|
|
||||||
use futures::future::Future;
|
use futures::future::Future;
|
||||||
|
|
||||||
|
use hyper::header::HeaderValue;
|
||||||
use hyper::server::conn::AddrStream;
|
use hyper::server::conn::AddrStream;
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use hyper::{Body, Request, Response, Server};
|
use hyper::{Body, Request, Response, Server};
|
||||||
|
use hyper::{HeaderMap, StatusCode};
|
||||||
|
|
||||||
use opentelemetry::{
|
use opentelemetry::{
|
||||||
global,
|
global,
|
||||||
|
|
@ -19,26 +21,31 @@ use opentelemetry::{
|
||||||
use garage_util::error::Error as GarageError;
|
use garage_util::error::Error as GarageError;
|
||||||
use garage_util::metrics::{gen_trace_id, RecordDuration};
|
use garage_util::metrics::{gen_trace_id, RecordDuration};
|
||||||
|
|
||||||
use crate::error::*;
|
|
||||||
|
|
||||||
pub(crate) trait ApiEndpoint: Send + Sync + 'static {
|
pub(crate) trait ApiEndpoint: Send + Sync + 'static {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
fn add_span_attributes(&self, span: SpanRef<'_>);
|
fn add_span_attributes(&self, span: SpanRef<'_>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ApiError: std::error::Error + Send + Sync + 'static {
|
||||||
|
fn http_status_code(&self) -> StatusCode;
|
||||||
|
fn add_http_headers(&self, header_map: &mut HeaderMap<HeaderValue>);
|
||||||
|
fn http_body(&self, garage_region: &str, path: &str) -> Body;
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub(crate) trait ApiHandler: Send + Sync + 'static {
|
pub(crate) trait ApiHandler: Send + Sync + 'static {
|
||||||
const API_NAME: &'static str;
|
const API_NAME: &'static str;
|
||||||
const API_NAME_DISPLAY: &'static str;
|
const API_NAME_DISPLAY: &'static str;
|
||||||
|
|
||||||
type Endpoint: ApiEndpoint;
|
type Endpoint: ApiEndpoint;
|
||||||
|
type Error: ApiError;
|
||||||
|
|
||||||
fn parse_endpoint(&self, r: &Request<Body>) -> Result<Self::Endpoint, Error>;
|
fn parse_endpoint(&self, r: &Request<Body>) -> Result<Self::Endpoint, Self::Error>;
|
||||||
async fn handle(
|
async fn handle(
|
||||||
&self,
|
&self,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
endpoint: Self::Endpoint,
|
endpoint: Self::Endpoint,
|
||||||
) -> Result<Response<Body>, Error>;
|
) -> Result<Response<Body>, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct ApiServer<A: ApiHandler> {
|
pub(crate) struct ApiServer<A: ApiHandler> {
|
||||||
|
|
@ -142,13 +149,13 @@ impl<A: ApiHandler> ApiServer<A> {
|
||||||
Ok(x)
|
Ok(x)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let body: Body = Body::from(e.aws_xml(&self.region, uri.path()));
|
let body: Body = e.http_body(&self.region, uri.path());
|
||||||
let mut http_error_builder = Response::builder()
|
let mut http_error_builder = Response::builder()
|
||||||
.status(e.http_status_code())
|
.status(e.http_status_code())
|
||||||
.header("Content-Type", "application/xml");
|
.header("Content-Type", "application/xml");
|
||||||
|
|
||||||
if let Some(header_map) = http_error_builder.headers_mut() {
|
if let Some(header_map) = http_error_builder.headers_mut() {
|
||||||
e.add_headers(header_map)
|
e.add_http_headers(header_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
let http_error = http_error_builder.body(body)?;
|
let http_error = http_error_builder.body(body)?;
|
||||||
|
|
@ -163,7 +170,7 @@ impl<A: ApiHandler> ApiServer<A> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handler_stage2(&self, req: Request<Body>) -> Result<Response<Body>, Error> {
|
async fn handler_stage2(&self, req: Request<Body>) -> Result<Response<Body>, A::Error> {
|
||||||
let endpoint = self.api_handler.parse_endpoint(&req)?;
|
let endpoint = self.api_handler.parse_endpoint(&req)?;
|
||||||
debug!("Endpoint: {}", endpoint.name());
|
debug!("Endpoint: {}", endpoint.name());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
|
use hyper::{Body, Request};
|
||||||
use idna::domain_to_unicode;
|
use idna::domain_to_unicode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use garage_util::data::*;
|
use crate::common_error::{CommonError as Error, *};
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
|
||||||
use garage_model::key_table::Key;
|
|
||||||
|
|
||||||
use crate::error::*;
|
|
||||||
|
|
||||||
/// What kind of authorization is required to perform a given action
|
/// What kind of authorization is required to perform a given action
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|
@ -50,7 +47,7 @@ pub fn authority_to_host(authority: &str) -> Result<String, Error> {
|
||||||
let mut iter = authority.chars().enumerate();
|
let mut iter = authority.chars().enumerate();
|
||||||
let (_, first_char) = iter
|
let (_, first_char) = iter
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| Error::BadRequest("Authority is empty".to_string()))?;
|
.ok_or_else(|| Error::bad_request("Authority is empty".to_string()))?;
|
||||||
|
|
||||||
let split = match first_char {
|
let split = match first_char {
|
||||||
'[' => {
|
'[' => {
|
||||||
|
|
@ -58,7 +55,7 @@ pub fn authority_to_host(authority: &str) -> Result<String, Error> {
|
||||||
match iter.next() {
|
match iter.next() {
|
||||||
Some((_, ']')) => iter.next(),
|
Some((_, ']')) => iter.next(),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Error::BadRequest(format!(
|
return Err(Error::bad_request(format!(
|
||||||
"Authority {} has an illegal format",
|
"Authority {} has an illegal format",
|
||||||
authority
|
authority
|
||||||
)))
|
)))
|
||||||
|
|
@ -71,7 +68,7 @@ pub fn authority_to_host(authority: &str) -> Result<String, Error> {
|
||||||
let authority = match split {
|
let authority = match split {
|
||||||
Some((i, ':')) => Ok(&authority[..i]),
|
Some((i, ':')) => Ok(&authority[..i]),
|
||||||
None => Ok(authority),
|
None => Ok(authority),
|
||||||
Some((_, _)) => Err(Error::BadRequest(format!(
|
Some((_, _)) => Err(Error::bad_request(format!(
|
||||||
"Authority {} has an illegal format",
|
"Authority {} has an illegal format",
|
||||||
authority
|
authority
|
||||||
))),
|
))),
|
||||||
|
|
@ -79,28 +76,6 @@ pub fn authority_to_host(authority: &str) -> Result<String, Error> {
|
||||||
authority.map(|h| domain_to_unicode(h).0)
|
authority.map(|h| domain_to_unicode(h).0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::ptr_arg)]
|
|
||||||
pub async fn resolve_bucket(
|
|
||||||
garage: &Garage,
|
|
||||||
bucket_name: &String,
|
|
||||||
api_key: &Key,
|
|
||||||
) -> Result<Uuid, Error> {
|
|
||||||
let api_key_params = api_key
|
|
||||||
.state
|
|
||||||
.as_option()
|
|
||||||
.ok_or_internal_error("Key should not be deleted at this point")?;
|
|
||||||
|
|
||||||
if let Some(Some(bucket_id)) = api_key_params.local_aliases.get(bucket_name) {
|
|
||||||
Ok(*bucket_id)
|
|
||||||
} else {
|
|
||||||
Ok(garage
|
|
||||||
.bucket_helper()
|
|
||||||
.resolve_global_bucket_name(bucket_name)
|
|
||||||
.await?
|
|
||||||
.ok_or(Error::NoSuchBucket)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract the bucket name and the key name from an HTTP path and possibly a bucket provided in
|
/// Extract the bucket name and the key name from an HTTP path and possibly a bucket provided in
|
||||||
/// the host header of the request
|
/// the host header of the request
|
||||||
///
|
///
|
||||||
|
|
@ -132,7 +107,7 @@ pub fn parse_bucket_key<'a>(
|
||||||
None => (path, None),
|
None => (path, None),
|
||||||
};
|
};
|
||||||
if bucket.is_empty() {
|
if bucket.is_empty() {
|
||||||
return Err(Error::BadRequest("No bucket specified".to_string()));
|
return Err(Error::bad_request("No bucket specified"));
|
||||||
}
|
}
|
||||||
Ok((bucket, key))
|
Ok((bucket, key))
|
||||||
}
|
}
|
||||||
|
|
@ -163,6 +138,12 @@ pub fn key_after_prefix(pfx: &str) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn parse_json_body<T: for<'de> Deserialize<'de>>(req: Request<Body>) -> Result<T, Error> {
|
||||||
|
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||||
|
let resp: T = serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?;
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -298,3 +279,11 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(crate) struct CustomApiErrorBody {
|
||||||
|
pub(crate) code: String,
|
||||||
|
pub(crate) message: String,
|
||||||
|
pub(crate) region: String,
|
||||||
|
pub(crate) path: String,
|
||||||
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue