Compare commits
74 commits
optimal-la
...
main
Author | SHA1 | Date | |
---|---|---|---|
76230f2028 | |||
6775569525 | |||
1649002e2b | |||
822e344845 | |||
7f7d53cfa9 | |||
fd10200bec | |||
0c7ed0b0af | |||
1af4a5ed56 | |||
d1279e04f3 | |||
041b60ed1d | |||
f8d5409894 | |||
d6040e32a6 | |||
d7f90cabb0 | |||
687660b27f | |||
9d82196945 | |||
a51e8d94c6 | |||
de9d6cddf7 | |||
f7c65e830e | |||
0e61e3b6fb | |||
a0abf41762 | |||
2ac75018a1 | |||
980572a887 | |||
7a0014b6f7 | |||
edb0b9c1ee | |||
f58a813a36 | |||
defd7d9e63 | |||
533afcf4e1 | |||
5ea5fd2130 | |||
35f8e8e2fb | |||
d5a2502b09 | |||
d7868c48a4 | |||
280d1be7b1 | |||
2065f011ca | |||
243b7c9a1c | |||
a3afc761b6 | |||
19bdd1c799 | |||
448dcc5cf4 | |||
26121bb619 | |||
280330ac72 | |||
4d7b4d9d20 | |||
fc450ec13a | |||
379b2049f5 | |||
293139a94a | |||
54e800ef8d | |||
1e40c93fd0 | |||
0cfb56d33e | |||
c1fb65194c | |||
67941000ee | |||
60c26fbc62 | |||
e76dba9561 | |||
7fafd14a25 | |||
555a54ec40 | |||
fc8f795bba | |||
a7af0c8af9 | |||
bcc9772470 | |||
c4e4cc1156 | |||
05547f2ba6 | |||
39ac295eb7 | |||
cf23aee183 | |||
74ea449f4b | |||
eabb37b53f | |||
e7824faa17 | |||
|
8dfc909759 | ||
485109ea60 | |||
ebe8a41f2d | |||
dc50fa3b34 | |||
a976c9190c | |||
72a0f90070 | |||
d814deb806 | |||
6a09f16da7 | |||
23207d18a0 | |||
3024405a65 | |||
5f0928f89c | |||
0a01b34e81 |
1
.gitattributes
vendored
|
@ -1 +0,0 @@
|
||||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
|
18
Cargo.lock
generated
|
@ -1048,7 +1048,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage"
|
name = "garage"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert-json-diff",
|
"assert-json-diff",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
@ -1096,7 +1096,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_api"
|
name = "garage_api"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
"base64",
|
||||||
|
@ -1141,7 +1141,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_block"
|
name = "garage_block"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
|
@ -1167,7 +1167,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_db"
|
name = "garage_db"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 3.1.18",
|
"clap 3.1.18",
|
||||||
"err-derive",
|
"err-derive",
|
||||||
|
@ -1182,7 +1182,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_model"
|
name = "garage_model"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
@ -1210,7 +1210,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_rpc"
|
name = "garage_rpc"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
@ -1241,7 +1241,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_table"
|
name = "garage_table"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -1263,7 +1263,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_util"
|
name = "garage_util"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
@ -1294,7 +1294,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_web"
|
name = "garage_web"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"err-derive",
|
"err-derive",
|
||||||
"futures",
|
"futures",
|
||||||
|
|
183
Cargo.nix
|
@ -32,7 +32,7 @@ args@{
|
||||||
ignoreLockHash,
|
ignoreLockHash,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
nixifiedLockHash = "90b29705f5037c7e1b33f4650841f1266f2e86fa03d5d0c87ad80be7619985c7";
|
nixifiedLockHash = "463114c4544bfa9b442a43afc6b39eb588f5720825c7a246ba9188c4bdb52944";
|
||||||
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
|
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
|
||||||
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
|
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
|
||||||
lockHashIgnored = if ignoreLockHash
|
lockHashIgnored = if ignoreLockHash
|
||||||
|
@ -56,15 +56,15 @@ in
|
||||||
{
|
{
|
||||||
cargo2nixVersion = "0.11.0";
|
cargo2nixVersion = "0.11.0";
|
||||||
workspace = {
|
workspace = {
|
||||||
garage_db = rustPackages.unknown.garage_db."0.8.0";
|
garage_db = rustPackages.unknown.garage_db."0.8.1";
|
||||||
garage_util = rustPackages.unknown.garage_util."0.8.0";
|
garage_util = rustPackages.unknown.garage_util."0.8.1";
|
||||||
garage_rpc = rustPackages.unknown.garage_rpc."0.8.0";
|
garage_rpc = rustPackages.unknown.garage_rpc."0.8.1";
|
||||||
garage_table = rustPackages.unknown.garage_table."0.8.0";
|
garage_table = rustPackages.unknown.garage_table."0.8.1";
|
||||||
garage_block = rustPackages.unknown.garage_block."0.8.0";
|
garage_block = rustPackages.unknown.garage_block."0.8.1";
|
||||||
garage_model = rustPackages.unknown.garage_model."0.8.0";
|
garage_model = rustPackages.unknown.garage_model."0.8.1";
|
||||||
garage_api = rustPackages.unknown.garage_api."0.8.0";
|
garage_api = rustPackages.unknown.garage_api."0.8.1";
|
||||||
garage_web = rustPackages.unknown.garage_web."0.8.0";
|
garage_web = rustPackages.unknown.garage_web."0.8.1";
|
||||||
garage = rustPackages.unknown.garage."0.8.0";
|
garage = rustPackages.unknown.garage."0.8.1";
|
||||||
k2v-client = rustPackages.unknown.k2v-client."0.0.1";
|
k2v-client = rustPackages.unknown.k2v-client."0.0.1";
|
||||||
};
|
};
|
||||||
"registry+https://github.com/rust-lang/crates.io-index".addr2line."0.17.0" = overridableMkRustCrate (profileName: rec {
|
"registry+https://github.com/rust-lang/crates.io-index".addr2line."0.17.0" = overridableMkRustCrate (profileName: rec {
|
||||||
|
@ -946,20 +946,20 @@ 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 = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c"; };
|
src = fetchCratesIo { inherit name version; sha256 = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c"; };
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "alloc")
|
[ "alloc" ]
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "default")
|
[ "default" ]
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "lazy_static")
|
[ "lazy_static" ]
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "std")
|
[ "std" ]
|
||||||
];
|
];
|
||||||
dependencies = {
|
dependencies = {
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "cfg_if" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
|
cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "crossbeam_utils" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.8.8" { inherit profileName; }).out;
|
crossbeam_utils = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.8.8" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "lazy_static" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }).out;
|
lazy_static = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "memoffset" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memoffset."0.6.5" { inherit profileName; }).out;
|
memoffset = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memoffset."0.6.5" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "scopeguard" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".scopeguard."1.1.0" { inherit profileName; }).out;
|
scopeguard = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".scopeguard."1.1.0" { inherit profileName; }).out;
|
||||||
};
|
};
|
||||||
buildDependencies = {
|
buildDependencies = {
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "autocfg" else null } = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }).out;
|
autocfg = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }).out;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -995,7 +995,7 @@ 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 = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"; };
|
src = fetchCratesIo { inherit name version; sha256 = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"; };
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "default")
|
[ "default" ]
|
||||||
[ "lazy_static" ]
|
[ "lazy_static" ]
|
||||||
[ "std" ]
|
[ "std" ]
|
||||||
];
|
];
|
||||||
|
@ -1321,8 +1321,8 @@ 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 = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"; };
|
src = fetchCratesIo { inherit name version; sha256 = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"; };
|
||||||
dependencies = {
|
dependencies = {
|
||||||
${ if (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") && hostPlatform.isUnix then "libc" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }).out;
|
${ if hostPlatform.isUnix then "libc" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }).out;
|
||||||
${ if (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") && hostPlatform.isWindows then "winapi" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }).out;
|
${ if hostPlatform.isWindows then "winapi" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }).out;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1490,13 +1490,13 @@ 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 = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"; };
|
src = fetchCratesIo { inherit name version; sha256 = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"; };
|
||||||
dependencies = {
|
dependencies = {
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "byteorder" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".byteorder."1.4.3" { inherit profileName; }).out;
|
byteorder = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".byteorder."1.4.3" { inherit profileName; }).out;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"unknown".garage."0.8.0" = overridableMkRustCrate (profileName: rec {
|
"unknown".garage."0.8.1" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "garage";
|
name = "garage";
|
||||||
version = "0.8.0";
|
version = "0.8.1";
|
||||||
registry = "unknown";
|
registry = "unknown";
|
||||||
src = fetchCrateLocal (workspaceSrc + "/src/garage");
|
src = fetchCrateLocal (workspaceSrc + "/src/garage");
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
|
@ -1522,14 +1522,14 @@ in
|
||||||
bytesize = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytesize."1.1.0" { inherit profileName; }).out;
|
bytesize = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytesize."1.1.0" { inherit profileName; }).out;
|
||||||
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
||||||
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
||||||
garage_api = (rustPackages."unknown".garage_api."0.8.0" { inherit profileName; }).out;
|
garage_api = (rustPackages."unknown".garage_api."0.8.1" { inherit profileName; }).out;
|
||||||
garage_block = (rustPackages."unknown".garage_block."0.8.0" { inherit profileName; }).out;
|
garage_block = (rustPackages."unknown".garage_block."0.8.1" { inherit profileName; }).out;
|
||||||
garage_db = (rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }).out;
|
garage_db = (rustPackages."unknown".garage_db."0.8.1" { inherit profileName; }).out;
|
||||||
garage_model = (rustPackages."unknown".garage_model."0.8.0" { inherit profileName; }).out;
|
garage_model = (rustPackages."unknown".garage_model."0.8.1" { inherit profileName; }).out;
|
||||||
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }).out;
|
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.1" { inherit profileName; }).out;
|
||||||
garage_table = (rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }).out;
|
garage_table = (rustPackages."unknown".garage_table."0.8.1" { inherit profileName; }).out;
|
||||||
garage_util = (rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }).out;
|
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out;
|
||||||
garage_web = (rustPackages."unknown".garage_web."0.8.0" { inherit profileName; }).out;
|
garage_web = (rustPackages."unknown".garage_web."0.8.1" { inherit profileName; }).out;
|
||||||
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
||||||
sodiumoxide = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }).out;
|
sodiumoxide = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".kuska-sodiumoxide."0.2.5-0" { inherit profileName; }).out;
|
||||||
netapp = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.2" { inherit profileName; }).out;
|
netapp = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.2" { inherit profileName; }).out;
|
||||||
|
@ -1563,9 +1563,9 @@ in
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"unknown".garage_api."0.8.0" = overridableMkRustCrate (profileName: rec {
|
"unknown".garage_api."0.8.1" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "garage_api";
|
name = "garage_api";
|
||||||
version = "0.8.0";
|
version = "0.8.1";
|
||||||
registry = "unknown";
|
registry = "unknown";
|
||||||
src = fetchCrateLocal (workspaceSrc + "/src/api");
|
src = fetchCrateLocal (workspaceSrc + "/src/api");
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
|
@ -1584,11 +1584,11 @@ in
|
||||||
form_urlencoded = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".form_urlencoded."1.0.1" { inherit profileName; }).out;
|
form_urlencoded = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".form_urlencoded."1.0.1" { inherit profileName; }).out;
|
||||||
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
||||||
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
||||||
garage_block = (rustPackages."unknown".garage_block."0.8.0" { inherit profileName; }).out;
|
garage_block = (rustPackages."unknown".garage_block."0.8.1" { inherit profileName; }).out;
|
||||||
garage_model = (rustPackages."unknown".garage_model."0.8.0" { inherit profileName; }).out;
|
garage_model = (rustPackages."unknown".garage_model."0.8.1" { inherit profileName; }).out;
|
||||||
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }).out;
|
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.1" { inherit profileName; }).out;
|
||||||
garage_table = (rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }).out;
|
garage_table = (rustPackages."unknown".garage_table."0.8.1" { inherit profileName; }).out;
|
||||||
garage_util = (rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }).out;
|
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out;
|
||||||
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
||||||
hmac = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.12.1" { inherit profileName; }).out;
|
hmac = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.12.1" { inherit profileName; }).out;
|
||||||
http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.8" { inherit profileName; }).out;
|
http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.8" { inherit profileName; }).out;
|
||||||
|
@ -1617,9 +1617,9 @@ in
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"unknown".garage_block."0.8.0" = overridableMkRustCrate (profileName: rec {
|
"unknown".garage_block."0.8.1" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "garage_block";
|
name = "garage_block";
|
||||||
version = "0.8.0";
|
version = "0.8.1";
|
||||||
registry = "unknown";
|
registry = "unknown";
|
||||||
src = fetchCrateLocal (workspaceSrc + "/src/block");
|
src = fetchCrateLocal (workspaceSrc + "/src/block");
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
|
@ -1632,10 +1632,10 @@ in
|
||||||
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }).out;
|
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }).out;
|
||||||
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
||||||
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
||||||
garage_db = (rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }).out;
|
garage_db = (rustPackages."unknown".garage_db."0.8.1" { inherit profileName; }).out;
|
||||||
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }).out;
|
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.1" { inherit profileName; }).out;
|
||||||
garage_table = (rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }).out;
|
garage_table = (rustPackages."unknown".garage_table."0.8.1" { inherit profileName; }).out;
|
||||||
garage_util = (rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }).out;
|
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out;
|
||||||
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
||||||
opentelemetry = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }).out;
|
opentelemetry = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }).out;
|
||||||
rand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }).out;
|
rand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }).out;
|
||||||
|
@ -1649,20 +1649,21 @@ in
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"unknown".garage_db."0.8.0" = overridableMkRustCrate (profileName: rec {
|
"unknown".garage_db."0.8.1" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "garage_db";
|
name = "garage_db";
|
||||||
version = "0.8.0";
|
version = "0.8.1";
|
||||||
registry = "unknown";
|
registry = "unknown";
|
||||||
src = fetchCrateLocal (workspaceSrc + "/src/db");
|
src = fetchCrateLocal (workspaceSrc + "/src/db");
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
(lib.optional (rootFeatures' ? "garage/bundled-libs" || rootFeatures' ? "garage/default" || rootFeatures' ? "garage_db/bundled-libs") "bundled-libs")
|
(lib.optional (rootFeatures' ? "garage/bundled-libs" || rootFeatures' ? "garage/default" || rootFeatures' ? "garage_db/bundled-libs") "bundled-libs")
|
||||||
(lib.optional (rootFeatures' ? "garage_db/clap" || rootFeatures' ? "garage_db/cli") "clap")
|
(lib.optional (rootFeatures' ? "garage_db/clap" || rootFeatures' ? "garage_db/cli") "clap")
|
||||||
(lib.optional (rootFeatures' ? "garage_db/cli") "cli")
|
(lib.optional (rootFeatures' ? "garage_db/cli") "cli")
|
||||||
|
[ "default" ]
|
||||||
(lib.optional (rootFeatures' ? "garage/lmdb" || rootFeatures' ? "garage_db/heed" || rootFeatures' ? "garage_db/lmdb" || rootFeatures' ? "garage_model/lmdb") "heed")
|
(lib.optional (rootFeatures' ? "garage/lmdb" || rootFeatures' ? "garage_db/heed" || rootFeatures' ? "garage_db/lmdb" || rootFeatures' ? "garage_model/lmdb") "heed")
|
||||||
(lib.optional (rootFeatures' ? "garage/lmdb" || rootFeatures' ? "garage_db/lmdb" || rootFeatures' ? "garage_model/lmdb") "lmdb")
|
(lib.optional (rootFeatures' ? "garage/lmdb" || rootFeatures' ? "garage_db/lmdb" || rootFeatures' ? "garage_model/lmdb") "lmdb")
|
||||||
(lib.optional (rootFeatures' ? "garage_db/cli" || rootFeatures' ? "garage_db/pretty_env_logger") "pretty_env_logger")
|
(lib.optional (rootFeatures' ? "garage_db/cli" || rootFeatures' ? "garage_db/pretty_env_logger") "pretty_env_logger")
|
||||||
(lib.optional (rootFeatures' ? "garage/bundled-libs" || rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sqlite" || rootFeatures' ? "garage_db/bundled-libs" || rootFeatures' ? "garage_db/rusqlite" || rootFeatures' ? "garage_db/sqlite" || rootFeatures' ? "garage_model/sqlite") "rusqlite")
|
(lib.optional (rootFeatures' ? "garage/bundled-libs" || rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sqlite" || rootFeatures' ? "garage_db/bundled-libs" || rootFeatures' ? "garage_db/rusqlite" || rootFeatures' ? "garage_db/sqlite" || rootFeatures' ? "garage_model/sqlite") "rusqlite")
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "sled")
|
[ "sled" ]
|
||||||
(lib.optional (rootFeatures' ? "garage/sqlite" || rootFeatures' ? "garage_db/sqlite" || rootFeatures' ? "garage_model/sqlite") "sqlite")
|
(lib.optional (rootFeatures' ? "garage/sqlite" || rootFeatures' ? "garage_db/sqlite" || rootFeatures' ? "garage_model/sqlite") "sqlite")
|
||||||
];
|
];
|
||||||
dependencies = {
|
dependencies = {
|
||||||
|
@ -1672,7 +1673,7 @@ in
|
||||||
hexdump = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }).out;
|
hexdump = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage_db/cli" || rootFeatures' ? "garage_db/pretty_env_logger" then "pretty_env_logger" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; }).out;
|
${ if rootFeatures' ? "garage_db/cli" || rootFeatures' ? "garage_db/pretty_env_logger" then "pretty_env_logger" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pretty_env_logger."0.4.0" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/bundled-libs" || rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sqlite" || rootFeatures' ? "garage_db/bundled-libs" || rootFeatures' ? "garage_db/rusqlite" || rootFeatures' ? "garage_db/sqlite" || rootFeatures' ? "garage_model/sqlite" then "rusqlite" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusqlite."0.27.0" { inherit profileName; }).out;
|
${ if rootFeatures' ? "garage/bundled-libs" || rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sqlite" || rootFeatures' ? "garage_db/bundled-libs" || rootFeatures' ? "garage_db/rusqlite" || rootFeatures' ? "garage_db/sqlite" || rootFeatures' ? "garage_model/sqlite" then "rusqlite" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusqlite."0.27.0" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "sled" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }).out;
|
sled = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { inherit profileName; }).out;
|
||||||
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }).out;
|
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.32" { inherit profileName; }).out;
|
||||||
};
|
};
|
||||||
devDependencies = {
|
devDependencies = {
|
||||||
|
@ -1680,15 +1681,16 @@ in
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"unknown".garage_model."0.8.0" = overridableMkRustCrate (profileName: rec {
|
"unknown".garage_model."0.8.1" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "garage_model";
|
name = "garage_model";
|
||||||
version = "0.8.0";
|
version = "0.8.1";
|
||||||
registry = "unknown";
|
registry = "unknown";
|
||||||
src = fetchCrateLocal (workspaceSrc + "/src/model");
|
src = fetchCrateLocal (workspaceSrc + "/src/model");
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
|
[ "default" ]
|
||||||
(lib.optional (rootFeatures' ? "garage/k2v" || rootFeatures' ? "garage_api/k2v" || rootFeatures' ? "garage_model/k2v") "k2v")
|
(lib.optional (rootFeatures' ? "garage/k2v" || rootFeatures' ? "garage_api/k2v" || rootFeatures' ? "garage_model/k2v") "k2v")
|
||||||
(lib.optional (rootFeatures' ? "garage/lmdb" || rootFeatures' ? "garage_model/lmdb") "lmdb")
|
(lib.optional (rootFeatures' ? "garage/lmdb" || rootFeatures' ? "garage_model/lmdb") "lmdb")
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_model/sled") "sled")
|
[ "sled" ]
|
||||||
(lib.optional (rootFeatures' ? "garage/sqlite" || rootFeatures' ? "garage_model/sqlite") "sqlite")
|
(lib.optional (rootFeatures' ? "garage/sqlite" || rootFeatures' ? "garage_model/sqlite") "sqlite")
|
||||||
];
|
];
|
||||||
dependencies = {
|
dependencies = {
|
||||||
|
@ -1699,11 +1701,11 @@ in
|
||||||
err_derive = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }).out;
|
err_derive = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }).out;
|
||||||
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
||||||
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
||||||
garage_block = (rustPackages."unknown".garage_block."0.8.0" { inherit profileName; }).out;
|
garage_block = (rustPackages."unknown".garage_block."0.8.1" { inherit profileName; }).out;
|
||||||
garage_db = (rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }).out;
|
garage_db = (rustPackages."unknown".garage_db."0.8.1" { inherit profileName; }).out;
|
||||||
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }).out;
|
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.1" { inherit profileName; }).out;
|
||||||
garage_table = (rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }).out;
|
garage_table = (rustPackages."unknown".garage_table."0.8.1" { inherit profileName; }).out;
|
||||||
garage_util = (rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }).out;
|
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out;
|
||||||
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
||||||
netapp = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.2" { inherit profileName; }).out;
|
netapp = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".netapp."0.5.2" { inherit profileName; }).out;
|
||||||
opentelemetry = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }).out;
|
opentelemetry = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }).out;
|
||||||
|
@ -1717,9 +1719,9 @@ in
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"unknown".garage_rpc."0.8.0" = overridableMkRustCrate (profileName: rec {
|
"unknown".garage_rpc."0.8.1" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "garage_rpc";
|
name = "garage_rpc";
|
||||||
version = "0.8.0";
|
version = "0.8.1";
|
||||||
registry = "unknown";
|
registry = "unknown";
|
||||||
src = fetchCrateLocal (workspaceSrc + "/src/rpc");
|
src = fetchCrateLocal (workspaceSrc + "/src/rpc");
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
|
@ -1739,7 +1741,7 @@ in
|
||||||
${ if rootFeatures' ? "garage/consul-discovery" || rootFeatures' ? "garage_rpc/consul-discovery" || rootFeatures' ? "garage_rpc/err-derive" then "err_derive" else null } = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }).out;
|
${ if rootFeatures' ? "garage/consul-discovery" || rootFeatures' ? "garage_rpc/consul-discovery" || rootFeatures' ? "garage_rpc/err-derive" then "err_derive" else null } = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }).out;
|
||||||
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
||||||
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
||||||
garage_util = (rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }).out;
|
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out;
|
||||||
gethostname = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".gethostname."0.2.3" { inherit profileName; }).out;
|
gethostname = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".gethostname."0.2.3" { inherit profileName; }).out;
|
||||||
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/kubernetes-discovery" || rootFeatures' ? "garage_rpc/k8s-openapi" || rootFeatures' ? "garage_rpc/kubernetes-discovery" then "k8s_openapi" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.16.0" { inherit profileName; }).out;
|
${ if rootFeatures' ? "garage/kubernetes-discovery" || rootFeatures' ? "garage_rpc/k8s-openapi" || rootFeatures' ? "garage_rpc/kubernetes-discovery" then "k8s_openapi" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".k8s-openapi."0.16.0" { inherit profileName; }).out;
|
||||||
|
@ -1761,9 +1763,9 @@ in
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"unknown".garage_table."0.8.0" = overridableMkRustCrate (profileName: rec {
|
"unknown".garage_table."0.8.1" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "garage_table";
|
name = "garage_table";
|
||||||
version = "0.8.0";
|
version = "0.8.1";
|
||||||
registry = "unknown";
|
registry = "unknown";
|
||||||
src = fetchCrateLocal (workspaceSrc + "/src/table");
|
src = fetchCrateLocal (workspaceSrc + "/src/table");
|
||||||
dependencies = {
|
dependencies = {
|
||||||
|
@ -1771,9 +1773,9 @@ in
|
||||||
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }).out;
|
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.2.0" { inherit profileName; }).out;
|
||||||
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
||||||
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.21" { inherit profileName; }).out;
|
||||||
garage_db = (rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }).out;
|
garage_db = (rustPackages."unknown".garage_db."0.8.1" { inherit profileName; }).out;
|
||||||
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }).out;
|
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.1" { inherit profileName; }).out;
|
||||||
garage_util = (rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }).out;
|
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out;
|
||||||
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
||||||
hexdump = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }).out;
|
hexdump = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hexdump."0.1.1" { inherit profileName; }).out;
|
||||||
opentelemetry = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }).out;
|
opentelemetry = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }).out;
|
||||||
|
@ -1786,9 +1788,9 @@ in
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"unknown".garage_util."0.8.0" = overridableMkRustCrate (profileName: rec {
|
"unknown".garage_util."0.8.1" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "garage_util";
|
name = "garage_util";
|
||||||
version = "0.8.0";
|
version = "0.8.1";
|
||||||
registry = "unknown";
|
registry = "unknown";
|
||||||
src = fetchCrateLocal (workspaceSrc + "/src/util");
|
src = fetchCrateLocal (workspaceSrc + "/src/util");
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
|
@ -1803,7 +1805,7 @@ in
|
||||||
digest = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.10.3" { inherit profileName; }).out;
|
digest = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".digest."0.10.3" { inherit profileName; }).out;
|
||||||
err_derive = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }).out;
|
err_derive = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }).out;
|
||||||
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
||||||
garage_db = (rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }).out;
|
garage_db = (rustPackages."unknown".garage_db."0.8.1" { inherit profileName; }).out;
|
||||||
git_version = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }).out;
|
git_version = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version."0.3.5" { inherit profileName; }).out;
|
||||||
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
|
||||||
http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.8" { inherit profileName; }).out;
|
http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.8" { inherit profileName; }).out;
|
||||||
|
@ -1823,18 +1825,18 @@ in
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"unknown".garage_web."0.8.0" = overridableMkRustCrate (profileName: rec {
|
"unknown".garage_web."0.8.1" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "garage_web";
|
name = "garage_web";
|
||||||
version = "0.8.0";
|
version = "0.8.1";
|
||||||
registry = "unknown";
|
registry = "unknown";
|
||||||
src = fetchCrateLocal (workspaceSrc + "/src/web");
|
src = fetchCrateLocal (workspaceSrc + "/src/web");
|
||||||
dependencies = {
|
dependencies = {
|
||||||
err_derive = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }).out;
|
err_derive = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".err-derive."0.3.1" { profileName = "__noProfile"; }).out;
|
||||||
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.21" { inherit profileName; }).out;
|
||||||
garage_api = (rustPackages."unknown".garage_api."0.8.0" { inherit profileName; }).out;
|
garage_api = (rustPackages."unknown".garage_api."0.8.1" { inherit profileName; }).out;
|
||||||
garage_model = (rustPackages."unknown".garage_model."0.8.0" { inherit profileName; }).out;
|
garage_model = (rustPackages."unknown".garage_model."0.8.1" { inherit profileName; }).out;
|
||||||
garage_table = (rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }).out;
|
garage_table = (rustPackages."unknown".garage_table."0.8.1" { inherit profileName; }).out;
|
||||||
garage_util = (rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }).out;
|
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out;
|
||||||
http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.8" { inherit profileName; }).out;
|
http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.8" { inherit profileName; }).out;
|
||||||
hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }).out;
|
hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.18" { inherit profileName; }).out;
|
||||||
opentelemetry = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }).out;
|
opentelemetry = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }).out;
|
||||||
|
@ -2448,7 +2450,7 @@ in
|
||||||
dependencies = {
|
dependencies = {
|
||||||
base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }).out;
|
base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.13.0" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "k2v-client/clap" || rootFeatures' ? "k2v-client/cli" then "clap" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".clap."3.1.18" { inherit profileName; }).out;
|
${ if rootFeatures' ? "k2v-client/clap" || rootFeatures' ? "k2v-client/cli" then "clap" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".clap."3.1.18" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "k2v-client/cli" || rootFeatures' ? "k2v-client/garage_util" then "garage_util" else null } = (rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }).out;
|
${ if rootFeatures' ? "k2v-client/cli" || rootFeatures' ? "k2v-client/garage_util" then "garage_util" else null } = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out;
|
||||||
http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.8" { inherit profileName; }).out;
|
http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.8" { inherit profileName; }).out;
|
||||||
log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }).out;
|
log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }).out;
|
||||||
rusoto_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_core."0.48.0" { inherit profileName; }).out;
|
rusoto_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rusoto_core."0.48.0" { inherit profileName; }).out;
|
||||||
|
@ -2846,10 +2848,10 @@ 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 = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"; };
|
src = fetchCratesIo { inherit name version; sha256 = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"; };
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "default")
|
[ "default" ]
|
||||||
];
|
];
|
||||||
buildDependencies = {
|
buildDependencies = {
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "autocfg" else null } = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }).out;
|
autocfg = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }).out;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4732,18 +4734,18 @@ 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 = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"; };
|
src = fetchCratesIo { inherit name version; sha256 = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"; };
|
||||||
features = builtins.concatLists [
|
features = builtins.concatLists [
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "default")
|
[ "default" ]
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "no_metrics")
|
[ "no_metrics" ]
|
||||||
];
|
];
|
||||||
dependencies = {
|
dependencies = {
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "crc32fast" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.3.2" { inherit profileName; }).out;
|
crc32fast = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.3.2" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "crossbeam_epoch" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-epoch."0.9.8" { inherit profileName; }).out;
|
crossbeam_epoch = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-epoch."0.9.8" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "crossbeam_utils" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.8.8" { inherit profileName; }).out;
|
crossbeam_utils = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.8.8" { inherit profileName; }).out;
|
||||||
${ if (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") && (hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "windows") then "fs2" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fs2."0.4.3" { inherit profileName; }).out;
|
${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "windows" then "fs2" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fs2."0.4.3" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "fxhash" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fxhash."0.2.1" { inherit profileName; }).out;
|
fxhash = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fxhash."0.2.1" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "libc" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }).out;
|
libc = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "log" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }).out;
|
log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }).out;
|
||||||
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled" then "parking_lot" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }).out;
|
parking_lot = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { inherit profileName; }).out;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5396,7 +5398,6 @@ in
|
||||||
[ "attributes" ]
|
[ "attributes" ]
|
||||||
[ "default" ]
|
[ "default" ]
|
||||||
[ "log" ]
|
[ "log" ]
|
||||||
[ "log-always" ]
|
|
||||||
[ "std" ]
|
[ "std" ]
|
||||||
[ "tracing-attributes" ]
|
[ "tracing-attributes" ]
|
||||||
];
|
];
|
||||||
|
@ -5890,7 +5891,7 @@ in
|
||||||
[ "ntstatus" ]
|
[ "ntstatus" ]
|
||||||
[ "objbase" ]
|
[ "objbase" ]
|
||||||
[ "processenv" ]
|
[ "processenv" ]
|
||||||
(lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "processthreadsapi")
|
[ "processthreadsapi" ]
|
||||||
[ "profileapi" ]
|
[ "profileapi" ]
|
||||||
[ "schannel" ]
|
[ "schannel" ]
|
||||||
[ "securitybaseapi" ]
|
[ "securitybaseapi" ]
|
||||||
|
|
|
@ -11,14 +11,14 @@ let
|
||||||
|
|
||||||
build_debug_and_release = (target: {
|
build_debug_and_release = (target: {
|
||||||
debug = (compile {
|
debug = (compile {
|
||||||
inherit target git_version;
|
inherit system target git_version pkgsSrc cargo2nixOverlay;
|
||||||
release = false;
|
release = false;
|
||||||
}).workspace.garage {
|
}).workspace.garage {
|
||||||
compileMode = "build";
|
compileMode = "build";
|
||||||
};
|
};
|
||||||
|
|
||||||
release = (compile {
|
release = (compile {
|
||||||
inherit target git_version;
|
inherit system target git_version pkgsSrc cargo2nixOverlay;
|
||||||
release = true;
|
release = true;
|
||||||
}).workspace.garage {
|
}).workspace.garage {
|
||||||
compileMode = "build";
|
compileMode = "build";
|
||||||
|
@ -39,7 +39,7 @@ in {
|
||||||
};
|
};
|
||||||
test = {
|
test = {
|
||||||
amd64 = test (compile {
|
amd64 = test (compile {
|
||||||
inherit git_version;
|
inherit system git_version pkgsSrc cargo2nixOverlay;
|
||||||
target = "x86_64-unknown-linux-musl";
|
target = "x86_64-unknown-linux-musl";
|
||||||
features = [
|
features = [
|
||||||
"garage/bundled-libs"
|
"garage/bundled-libs"
|
||||||
|
@ -52,7 +52,7 @@ in {
|
||||||
};
|
};
|
||||||
clippy = {
|
clippy = {
|
||||||
amd64 = (compile {
|
amd64 = (compile {
|
||||||
inherit git_version;
|
inherit system git_version pkgsSrc cargo2nixOverlay;
|
||||||
target = "x86_64-unknown-linux-musl";
|
target = "x86_64-unknown-linux-musl";
|
||||||
compiler = "clippy";
|
compiler = "clippy";
|
||||||
}).workspace.garage {
|
}).workspace.garage {
|
||||||
|
|
17
doc/api/README.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Browse doc
|
||||||
|
|
||||||
|
Run in this directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 -m http.server
|
||||||
|
```
|
||||||
|
|
||||||
|
And open in your browser:
|
||||||
|
- http://localhost:8000/garage-admin-v0.html
|
||||||
|
|
||||||
|
# Validate doc
|
||||||
|
|
||||||
|
```
|
||||||
|
wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.1.0/openapi-generator-cli-6.1.0.jar -O openapi-generator-cli.jar
|
||||||
|
java -jar openapi-generator-cli.jar validate -i garage-admin-v0.yml
|
||||||
|
```
|
59
doc/api/css/redoc.css
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/* montserrat-300 - latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Montserrat';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local(''),
|
||||||
|
url('../fonts/montserrat-v25-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||||
|
url('../fonts/montserrat-v25-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* montserrat-regular - latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Montserrat';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local(''),
|
||||||
|
url('../fonts/montserrat-v25-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||||
|
url('../fonts/montserrat-v25-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* montserrat-700 - latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Montserrat';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local(''),
|
||||||
|
url('../fonts/montserrat-v25-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||||
|
url('../fonts/montserrat-v25-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
|
}
|
||||||
|
/* roboto-300 - latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local(''),
|
||||||
|
url('../fonts/roboto-v30-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||||
|
url('../fonts/roboto-v30-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* roboto-regular - latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local(''),
|
||||||
|
url('../fonts/roboto-v30-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||||
|
url('../fonts/roboto-v30-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* roboto-700 - latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local(''),
|
||||||
|
url('../fonts/roboto-v30-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||||
|
url('../fonts/roboto-v30-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
|
}
|
||||||
|
|
BIN
doc/api/fonts/montserrat-v25-latin-300.woff
Normal file
BIN
doc/api/fonts/montserrat-v25-latin-300.woff2
Normal file
BIN
doc/api/fonts/montserrat-v25-latin-700.woff
Normal file
BIN
doc/api/fonts/montserrat-v25-latin-700.woff2
Normal file
BIN
doc/api/fonts/montserrat-v25-latin-regular.woff
Normal file
BIN
doc/api/fonts/montserrat-v25-latin-regular.woff2
Normal file
BIN
doc/api/fonts/roboto-v30-latin-300.woff
Normal file
BIN
doc/api/fonts/roboto-v30-latin-300.woff2
Normal file
BIN
doc/api/fonts/roboto-v30-latin-700.woff
Normal file
BIN
doc/api/fonts/roboto-v30-latin-700.woff2
Normal file
BIN
doc/api/fonts/roboto-v30-latin-regular.woff
Normal file
BIN
doc/api/fonts/roboto-v30-latin-regular.woff2
Normal file
24
doc/api/garage-admin-v0.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Garage Adminstration API v0</title>
|
||||||
|
<!-- needed for adaptive design -->
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="./css/redoc.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Redoc doesn't change outer page styles
|
||||||
|
-->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<redoc spec-url='./garage-admin-v0.yml'></redoc>
|
||||||
|
<script src="./redoc.standalone.js"> </script>
|
||||||
|
</body>
|
||||||
|
</html>
|
1212
doc/api/garage-admin-v0.yml
Normal file
1806
doc/api/redoc.standalone.js
Normal file
54
doc/book/build/_index.md
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
+++
|
||||||
|
title = "Build your own app"
|
||||||
|
weight = 4
|
||||||
|
sort_by = "weight"
|
||||||
|
template = "documentation.html"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Garage has many API that you can rely on to build complex applications.
|
||||||
|
In this section, we reference the existing SDKs and give some code examples.
|
||||||
|
|
||||||
|
|
||||||
|
## ⚠️ DISCLAIMER
|
||||||
|
|
||||||
|
**K2V AND ADMIN SDK ARE TECHNICAL PREVIEWS**. The following limitations apply:
|
||||||
|
- The API is not complete, some actions are possible only through the `garage` binary
|
||||||
|
- The underlying admin API is not yet stable nor complete, it can breaks at any time
|
||||||
|
- The generator configuration is currently tweaked, the library might break at any time due to a generator change
|
||||||
|
- Because the API and the library are not stable, none of them are published in a package manager (npm, pypi, etc.)
|
||||||
|
- This code has not been extensively tested, some things might not work (please report!)
|
||||||
|
|
||||||
|
To have the best experience possible, please consider:
|
||||||
|
- Make sure that the version of the library you are using is pinned (`go.sum`, `package-lock.json`, `requirements.txt`).
|
||||||
|
- Before upgrading your Garage cluster, make sure that you can find a version of this SDK that works with your targeted version and that you are able to update your own code to work with this new version of the library.
|
||||||
|
- Join our Matrix channel at `#garage:deuxfleurs.fr`, say that you are interested by this SDK, and report any friction.
|
||||||
|
- If stability is critical, mirror this repository on your own infrastructure, regenerate the SDKs and upgrade them at your own pace.
|
||||||
|
|
||||||
|
|
||||||
|
## About the APIs
|
||||||
|
|
||||||
|
Code can interact with Garage through 3 different APIs: S3, K2V, and Admin.
|
||||||
|
Each of them has a specific scope.
|
||||||
|
|
||||||
|
### S3
|
||||||
|
|
||||||
|
De-facto standard, introduced by Amazon, designed to store blobs of data.
|
||||||
|
|
||||||
|
### K2V
|
||||||
|
|
||||||
|
A simple database API similar to RiakKV or DynamoDB.
|
||||||
|
Think a key value store with some additional operations.
|
||||||
|
Its design is inspired by Distributed Hash Tables (DHT).
|
||||||
|
|
||||||
|
More information:
|
||||||
|
- [In the reference manual](@/documentation/reference-manual/k2v.md)
|
||||||
|
|
||||||
|
|
||||||
|
### Administration
|
||||||
|
|
||||||
|
Garage operations can also be automated through a REST API.
|
||||||
|
We are currently building this SDK for [Python](@/documentation/build/python.md#admin-api), [Javascript](@/documentation/build/javascript.md#administration) and [Golang](@/documentation/build/golang.md#administration).
|
||||||
|
|
||||||
|
More information:
|
||||||
|
- [In the reference manual](@/documentation/reference-manual/admin-api.md)
|
||||||
|
- [Full specifiction](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.html)
|
69
doc/book/build/golang.md
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
+++
|
||||||
|
title = "Golang"
|
||||||
|
weight = 30
|
||||||
|
+++
|
||||||
|
|
||||||
|
## S3
|
||||||
|
|
||||||
|
*Coming soon*
|
||||||
|
|
||||||
|
Some refs:
|
||||||
|
- Minio minio-go-sdk
|
||||||
|
- [Reference](https://docs.min.io/docs/golang-client-api-reference.html)
|
||||||
|
|
||||||
|
- Amazon aws-sdk-go-v2
|
||||||
|
- [Installation](https://aws.github.io/aws-sdk-go-v2/docs/getting-started/)
|
||||||
|
- [Reference](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3)
|
||||||
|
- [Example](https://aws.github.io/aws-sdk-go-v2/docs/code-examples/s3/putobject/)
|
||||||
|
|
||||||
|
## K2V
|
||||||
|
|
||||||
|
*Coming soon*
|
||||||
|
|
||||||
|
## Administration
|
||||||
|
|
||||||
|
Install the SDK with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang
|
||||||
|
```
|
||||||
|
|
||||||
|
A short example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Set Host and other parameters
|
||||||
|
configuration := garage.NewConfiguration()
|
||||||
|
configuration.Host = "127.0.0.1:3903"
|
||||||
|
|
||||||
|
|
||||||
|
// We can now generate a client
|
||||||
|
client := garage.NewAPIClient(configuration)
|
||||||
|
|
||||||
|
// Authentication is handled through the context pattern
|
||||||
|
ctx := context.WithValue(context.Background(), garage.ContextAccessToken, "s3cr3t")
|
||||||
|
|
||||||
|
// Send a request
|
||||||
|
resp, r, err := client.NodesApi.GetNodes(ctx).Execute()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error when calling `NodesApi.GetNodes``: %v\n", err)
|
||||||
|
fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the response
|
||||||
|
fmt.Fprintf(os.Stdout, "Target hostname: %v\n", resp.KnownNodes[resp.Node].Hostname)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See also:
|
||||||
|
- [generated doc](https://git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang)
|
||||||
|
- [examples](https://git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-generator/src/branch/main/example/golang)
|
55
doc/book/build/javascript.md
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
+++
|
||||||
|
title = "Javascript"
|
||||||
|
weight = 10
|
||||||
|
+++
|
||||||
|
|
||||||
|
## S3
|
||||||
|
|
||||||
|
*Coming soon*.
|
||||||
|
|
||||||
|
Some refs:
|
||||||
|
- Minio SDK
|
||||||
|
- [Reference](https://docs.min.io/docs/javascript-client-api-reference.html)
|
||||||
|
|
||||||
|
- Amazon aws-sdk-js
|
||||||
|
- [Installation](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-started.html)
|
||||||
|
- [Reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html)
|
||||||
|
- [Example](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/s3-example-creating-buckets.html)
|
||||||
|
|
||||||
|
## K2V
|
||||||
|
|
||||||
|
*Coming soon*
|
||||||
|
|
||||||
|
## Administration
|
||||||
|
|
||||||
|
Install the SDK with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save git+https://git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-js.git
|
||||||
|
```
|
||||||
|
|
||||||
|
A short example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const garage = require('garage_administration_api_v0garage_v0_8_0');
|
||||||
|
|
||||||
|
const api = new garage.ApiClient("http://127.0.0.1:3903/v0");
|
||||||
|
api.authentications['bearerAuth'].accessToken = "s3cr3t";
|
||||||
|
|
||||||
|
const [node, layout, key, bucket] = [
|
||||||
|
new garage.NodesApi(api),
|
||||||
|
new garage.LayoutApi(api),
|
||||||
|
new garage.KeyApi(api),
|
||||||
|
new garage.BucketApi(api),
|
||||||
|
];
|
||||||
|
|
||||||
|
node.getNodes().then((data) => {
|
||||||
|
console.log(`nodes: ${Object.values(data.knownNodes).map(n => n.hostname)}`)
|
||||||
|
}, (error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
See also:
|
||||||
|
- [sdk repository](https://git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-js)
|
||||||
|
- [examples](https://git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-generator/src/branch/main/example/javascript)
|
|
@ -1,8 +1,10 @@
|
||||||
+++
|
+++
|
||||||
title = "Your code (PHP, JS, Go...)"
|
title = "Others"
|
||||||
weight = 30
|
weight = 99
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
## S3
|
||||||
|
|
||||||
If you are developping a new application, you may want to use Garage to store your user's media.
|
If you are developping a new application, you may want to use Garage to store your user's media.
|
||||||
|
|
||||||
The S3 API that Garage uses is a standard REST API, so as long as you can make HTTP requests,
|
The S3 API that Garage uses is a standard REST API, so as long as you can make HTTP requests,
|
||||||
|
@ -13,44 +15,14 @@ Instead, there are some libraries already avalaible.
|
||||||
|
|
||||||
Some of them are maintained by Amazon, some by Minio, others by the community.
|
Some of them are maintained by Amazon, some by Minio, others by the community.
|
||||||
|
|
||||||
## PHP
|
### PHP
|
||||||
|
|
||||||
- Amazon aws-sdk-php
|
- Amazon aws-sdk-php
|
||||||
- [Installation](https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/getting-started_installation.html)
|
- [Installation](https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/getting-started_installation.html)
|
||||||
- [Reference](https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html)
|
- [Reference](https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html)
|
||||||
- [Example](https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-examples-creating-buckets.html)
|
- [Example](https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-examples-creating-buckets.html)
|
||||||
|
|
||||||
## Javascript
|
### Java
|
||||||
|
|
||||||
- Minio SDK
|
|
||||||
- [Reference](https://docs.min.io/docs/javascript-client-api-reference.html)
|
|
||||||
|
|
||||||
- Amazon aws-sdk-js
|
|
||||||
- [Installation](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-started.html)
|
|
||||||
- [Reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html)
|
|
||||||
- [Example](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/s3-example-creating-buckets.html)
|
|
||||||
|
|
||||||
## Golang
|
|
||||||
|
|
||||||
- Minio minio-go-sdk
|
|
||||||
- [Reference](https://docs.min.io/docs/golang-client-api-reference.html)
|
|
||||||
|
|
||||||
- Amazon aws-sdk-go-v2
|
|
||||||
- [Installation](https://aws.github.io/aws-sdk-go-v2/docs/getting-started/)
|
|
||||||
- [Reference](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3)
|
|
||||||
- [Example](https://aws.github.io/aws-sdk-go-v2/docs/code-examples/s3/putobject/)
|
|
||||||
|
|
||||||
## Python
|
|
||||||
|
|
||||||
- Minio SDK
|
|
||||||
- [Reference](https://docs.min.io/docs/python-client-api-reference.html)
|
|
||||||
|
|
||||||
- Amazon boto3
|
|
||||||
- [Installation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html)
|
|
||||||
- [Reference](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html)
|
|
||||||
- [Example](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-uploading-files.html)
|
|
||||||
|
|
||||||
## Java
|
|
||||||
|
|
||||||
- Minio SDK
|
- Minio SDK
|
||||||
- [Reference](https://docs.min.io/docs/java-client-api-reference.html)
|
- [Reference](https://docs.min.io/docs/java-client-api-reference.html)
|
||||||
|
@ -60,23 +32,18 @@ Some of them are maintained by Amazon, some by Minio, others by the community.
|
||||||
- [Reference](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/s3/S3Client.html)
|
- [Reference](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/s3/S3Client.html)
|
||||||
- [Example](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/examples-s3-objects.html)
|
- [Example](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/examples-s3-objects.html)
|
||||||
|
|
||||||
## Rust
|
### .NET
|
||||||
|
|
||||||
- Amazon aws-rust-sdk
|
|
||||||
- [Github](https://github.com/awslabs/aws-sdk-rust)
|
|
||||||
|
|
||||||
## .NET
|
|
||||||
|
|
||||||
- Minio SDK
|
- Minio SDK
|
||||||
- [Reference](https://docs.min.io/docs/dotnet-client-api-reference.html)
|
- [Reference](https://docs.min.io/docs/dotnet-client-api-reference.html)
|
||||||
|
|
||||||
- Amazon aws-dotnet-sdk
|
- Amazon aws-dotnet-sdk
|
||||||
|
|
||||||
## C++
|
### C++
|
||||||
|
|
||||||
- Amazon aws-cpp-sdk
|
- Amazon aws-cpp-sdk
|
||||||
|
|
||||||
## Haskell
|
### Haskell
|
||||||
|
|
||||||
- Minio SDK
|
- Minio SDK
|
||||||
- [Reference](https://docs.min.io/docs/haskell-client-api-reference.html)
|
- [Reference](https://docs.min.io/docs/haskell-client-api-reference.html)
|
138
doc/book/build/python.md
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
+++
|
||||||
|
title = "Python"
|
||||||
|
weight = 20
|
||||||
|
+++
|
||||||
|
|
||||||
|
## S3
|
||||||
|
|
||||||
|
### Using Minio SDK
|
||||||
|
|
||||||
|
First install the SDK:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip3 install minio
|
||||||
|
```
|
||||||
|
|
||||||
|
Then instantiate a client object using garage root domain, api key and secret:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import minio
|
||||||
|
|
||||||
|
client = minio.Minio(
|
||||||
|
"your.domain.tld",
|
||||||
|
"GKyourapikey",
|
||||||
|
"abcd[...]1234",
|
||||||
|
# Force the region, this is specific to garage
|
||||||
|
region="region",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use all the standard S3 endpoints as implemented by the Minio SDK:
|
||||||
|
|
||||||
|
```
|
||||||
|
# List buckets
|
||||||
|
print(client.list_buckets())
|
||||||
|
|
||||||
|
# Put an object containing 'content' to /path in bucket named 'bucket':
|
||||||
|
content = b"content"
|
||||||
|
client.put_object(
|
||||||
|
"bucket",
|
||||||
|
"path",
|
||||||
|
io.BytesIO(content),
|
||||||
|
len(content),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read the object back and check contents
|
||||||
|
data = client.get_object("bucket", "path").read()
|
||||||
|
assert data == content
|
||||||
|
```
|
||||||
|
|
||||||
|
For further documentation, see the Minio SDK
|
||||||
|
[Reference](https://docs.min.io/docs/python-client-api-reference.html)
|
||||||
|
|
||||||
|
### Using Amazon boto3
|
||||||
|
|
||||||
|
*Coming soon*
|
||||||
|
|
||||||
|
See the official documentation:
|
||||||
|
- [Installation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html)
|
||||||
|
- [Reference](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html)
|
||||||
|
- [Example](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-uploading-files.html)
|
||||||
|
|
||||||
|
## K2V
|
||||||
|
|
||||||
|
*Coming soon*
|
||||||
|
|
||||||
|
## Admin API
|
||||||
|
|
||||||
|
You need at least Python 3.6, pip, and setuptools.
|
||||||
|
Because the python package is in a subfolder, the command is a bit more complicated than usual:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip3 install --user 'git+https://git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-python'
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, let imagine you have a fresh Garage instance running on localhost, with the admin API configured on port 3903 with the bearer `s3cr3t`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import garage_admin_sdk
|
||||||
|
from garage_admin_sdk.apis import *
|
||||||
|
from garage_admin_sdk.models import *
|
||||||
|
|
||||||
|
configuration = garage_admin_sdk.Configuration(
|
||||||
|
host = "http://localhost:3903/v0",
|
||||||
|
access_token = "s3cr3t"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Init APIs
|
||||||
|
api = garage_admin_sdk.ApiClient(configuration)
|
||||||
|
nodes, layout, keys, buckets = NodesApi(api), LayoutApi(api), KeyApi(api), BucketApi(api)
|
||||||
|
|
||||||
|
# Display some info on the node
|
||||||
|
status = nodes.get_nodes()
|
||||||
|
print(f"running garage {status.garage_version}, node_id {status.node}")
|
||||||
|
|
||||||
|
# Change layout of this node
|
||||||
|
current = layout.get_layout()
|
||||||
|
layout.add_layout({
|
||||||
|
status.node: NodeClusterInfo(
|
||||||
|
zone = "dc1",
|
||||||
|
capacity = 1,
|
||||||
|
tags = [ "dev" ],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
layout.apply_layout(LayoutVersion(
|
||||||
|
version = current.version + 1
|
||||||
|
))
|
||||||
|
|
||||||
|
# Create key, allow it to create buckets
|
||||||
|
kinfo = keys.add_key(AddKeyRequest(name="openapi"))
|
||||||
|
|
||||||
|
allow_create = UpdateKeyRequestAllow(create_bucket=True)
|
||||||
|
keys.update_key(kinfo.access_key_id, UpdateKeyRequest(allow=allow_create))
|
||||||
|
|
||||||
|
# Create a bucket, allow key, set quotas
|
||||||
|
binfo = buckets.create_bucket(CreateBucketRequest(global_alias="documentation"))
|
||||||
|
binfo = buckets.allow_bucket_key(AllowBucketKeyRequest(
|
||||||
|
bucket_id=binfo.id,
|
||||||
|
access_key_id=kinfo.access_key_id,
|
||||||
|
permissions=AllowBucketKeyRequestPermissions(read=True, write=True, owner=True),
|
||||||
|
))
|
||||||
|
binfo = buckets.update_bucket(binfo.id, UpdateBucketRequest(
|
||||||
|
quotas=UpdateBucketRequestQuotas(max_size=19029801,max_objects=1500)))
|
||||||
|
|
||||||
|
# Display key
|
||||||
|
print(f"""
|
||||||
|
cluster ready
|
||||||
|
key id is {kinfo.access_key_id}
|
||||||
|
secret key is {kinfo.secret_access_key}
|
||||||
|
bucket {binfo.global_aliases[0]} contains {binfo.objects}/{binfo.quotas.max_objects} objects
|
||||||
|
""")
|
||||||
|
```
|
||||||
|
|
||||||
|
*This example is named `short.py` in the example folder. Other python examples are also available.*
|
||||||
|
|
||||||
|
See also:
|
||||||
|
- [sdk repo](https://git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-python)
|
||||||
|
- [examples](https://git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-generator/src/branch/main/example/python)
|
||||||
|
|
47
doc/book/build/rust.md
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
+++
|
||||||
|
title = "Rust"
|
||||||
|
weight = 40
|
||||||
|
+++
|
||||||
|
|
||||||
|
## S3
|
||||||
|
|
||||||
|
*Coming soon*
|
||||||
|
|
||||||
|
Some refs:
|
||||||
|
- Amazon aws-rust-sdk
|
||||||
|
- [Github](https://github.com/awslabs/aws-sdk-rust)
|
||||||
|
|
||||||
|
## K2V
|
||||||
|
|
||||||
|
*Coming soon*
|
||||||
|
|
||||||
|
Some refs: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/src/k2v-client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# all these values can be provided on the cli instead
|
||||||
|
export AWS_ACCESS_KEY_ID=GK123456
|
||||||
|
export AWS_SECRET_ACCESS_KEY=0123..789
|
||||||
|
export AWS_REGION=garage
|
||||||
|
export K2V_ENDPOINT=http://172.30.2.1:3903
|
||||||
|
export K2V_BUCKET=my-bucket
|
||||||
|
|
||||||
|
cargo run --features=cli -- read-range my-partition-key --all
|
||||||
|
|
||||||
|
cargo run --features=cli -- insert my-partition-key my-sort-key --text "my string1"
|
||||||
|
cargo run --features=cli -- insert my-partition-key my-sort-key --text "my string2"
|
||||||
|
cargo run --features=cli -- insert my-partition-key my-sort-key2 --text "my string"
|
||||||
|
|
||||||
|
cargo run --features=cli -- read-range my-partition-key --all
|
||||||
|
|
||||||
|
causality=$(cargo run --features=cli -- read my-partition-key my-sort-key2 -b | head -n1)
|
||||||
|
cargo run --features=cli -- delete my-partition-key my-sort-key2 -c $causality
|
||||||
|
|
||||||
|
causality=$(cargo run --features=cli -- read my-partition-key my-sort-key -b | head -n1)
|
||||||
|
cargo run --features=cli -- insert my-partition-key my-sort-key --text "my string3" -c $causality
|
||||||
|
|
||||||
|
cargo run --features=cli -- read-range my-partition-key --all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Admin API
|
||||||
|
|
||||||
|
*Coming soon*
|
|
@ -1,5 +1,5 @@
|
||||||
+++
|
+++
|
||||||
title = "Integrations"
|
title = "Existing integrations"
|
||||||
weight = 3
|
weight = 3
|
||||||
sort_by = "weight"
|
sort_by = "weight"
|
||||||
template = "documentation.html"
|
template = "documentation.html"
|
||||||
|
@ -14,7 +14,6 @@ In particular, you will find here instructions to connect it with:
|
||||||
- [Applications](@/documentation/connect/apps/index.md)
|
- [Applications](@/documentation/connect/apps/index.md)
|
||||||
- [Website hosting](@/documentation/connect/websites.md)
|
- [Website hosting](@/documentation/connect/websites.md)
|
||||||
- [Software repositories](@/documentation/connect/repositories.md)
|
- [Software repositories](@/documentation/connect/repositories.md)
|
||||||
- [Your own code](@/documentation/connect/code.md)
|
|
||||||
- [FUSE](@/documentation/connect/fs.md)
|
- [FUSE](@/documentation/connect/fs.md)
|
||||||
|
|
||||||
### Generic instructions
|
### Generic instructions
|
||||||
|
|
|
@ -8,7 +8,7 @@ In this section, we cover the following web applications:
|
||||||
| Name | Status | Note |
|
| Name | Status | Note |
|
||||||
|------|--------|------|
|
|------|--------|------|
|
||||||
| [Nextcloud](#nextcloud) | ✅ | Both Primary Storage and External Storage are supported |
|
| [Nextcloud](#nextcloud) | ✅ | Both Primary Storage and External Storage are supported |
|
||||||
| [Peertube](#peertube) | ✅ | Must be configured with the website endpoint |
|
| [Peertube](#peertube) | ✅ | Supported with the website endpoint, proxifying private videos unsupported |
|
||||||
| [Mastodon](#mastodon) | ✅ | Natively supported |
|
| [Mastodon](#mastodon) | ✅ | Natively supported |
|
||||||
| [Matrix](#matrix) | ✅ | Tested with `synapse-s3-storage-provider` |
|
| [Matrix](#matrix) | ✅ | Tested with `synapse-s3-storage-provider` |
|
||||||
| [Pixelfed](#pixelfed) | ❓ | Not yet tested |
|
| [Pixelfed](#pixelfed) | ❓ | Not yet tested |
|
||||||
|
@ -128,6 +128,10 @@ In other words, Peertube is only responsible of the "control plane" and offload
|
||||||
In return, this system is a bit harder to configure.
|
In return, this system is a bit harder to configure.
|
||||||
We show how it is still possible to configure Garage with Peertube, allowing you to spread the load and the bandwidth usage on the Garage cluster.
|
We show how it is still possible to configure Garage with Peertube, allowing you to spread the load and the bandwidth usage on the Garage cluster.
|
||||||
|
|
||||||
|
Starting from version 5.0, Peertube also supports improving the security for private videos by not exposing them directly
|
||||||
|
but relying on a single control point in the Peertube instance. This is based on S3 per-object and prefix ACL, which are not currently supported
|
||||||
|
in Garage, so this feature is unsupported. While this technically impedes security for private videos, it is not a blocking issue and could be
|
||||||
|
a reasonable trade-off for some instances.
|
||||||
|
|
||||||
### Create resources in Garage
|
### Create resources in Garage
|
||||||
|
|
||||||
|
@ -195,6 +199,11 @@ object_storage:
|
||||||
|
|
||||||
max_upload_part: 2GB
|
max_upload_part: 2GB
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
# You may enable this feature, yet it will not provide any security benefit, so
|
||||||
|
# you should rather benefit from Garage public endpoint for all videos
|
||||||
|
proxify_private_files: false
|
||||||
|
|
||||||
streaming_playlists:
|
streaming_playlists:
|
||||||
bucket_name: 'peertube-playlist'
|
bucket_name: 'peertube-playlist'
|
||||||
|
|
||||||
|
|
306
doc/book/cookbook/monitoring.md
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
+++
|
||||||
|
title = "Monitoring Garage"
|
||||||
|
weight = 40
|
||||||
|
+++
|
||||||
|
|
||||||
|
Garage exposes some internal metrics in the Prometheus data format.
|
||||||
|
This page explains how to exploit these metrics.
|
||||||
|
|
||||||
|
## Setting up monitoring
|
||||||
|
|
||||||
|
### Enabling the Admin API endpoint
|
||||||
|
|
||||||
|
If you have not already enabled the [administration API endpoint](@/documentation/reference-manual/admin-api.md), do so by adding the following lines to your configuration file:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[admin]
|
||||||
|
api_bind_addr = "0.0.0.0:3903"
|
||||||
|
```
|
||||||
|
|
||||||
|
This will allow anyone to scrape Prometheus metrics by fetching
|
||||||
|
`http://localhost:3093/metrics`. If you want to restrict access
|
||||||
|
to the exported metrics, set the `metrics_token` configuration value
|
||||||
|
to a bearer token to be used when fetching the metrics endpoint.
|
||||||
|
|
||||||
|
### Setting up Prometheus and Grafana
|
||||||
|
|
||||||
|
Add a scrape config to your Prometheus daemon to scrape metrics from
|
||||||
|
all of your nodes:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'garage'
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- 'node1.mycluster:3903'
|
||||||
|
- 'node2.mycluster:3903'
|
||||||
|
- 'node3.mycluster:3903'
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have set a metrics token in your Garage configuration file,
|
||||||
|
add the following lines in your Prometheus scrape config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
authorization:
|
||||||
|
type: Bearer
|
||||||
|
credentials: 'your metrics token'
|
||||||
|
```
|
||||||
|
|
||||||
|
To visualize the scraped data in Grafana,
|
||||||
|
you can either import our [Grafana dashboard for Garage](https://git.deuxfleurs.fr/Deuxfleurs/garage/raw/branch/main/script/telemetry/grafana-garage-dashboard-prometheus.json)
|
||||||
|
or make your own.
|
||||||
|
We detail below the list of exposed metrics and their meaning.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## List of exported metrics
|
||||||
|
|
||||||
|
|
||||||
|
### Metrics of the API endpoints
|
||||||
|
|
||||||
|
#### `api_admin_request_counter` (counter)
|
||||||
|
|
||||||
|
Counts the number of requests to a given endpoint of the administration API. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
api_admin_request_counter{api_endpoint="Metrics"} 127041
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api_admin_request_duration` (histogram)
|
||||||
|
|
||||||
|
Evaluates the duration of API calls to the various administration API endpoint. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
api_admin_request_duration_bucket{api_endpoint="Metrics",le="0.5"} 127041
|
||||||
|
api_admin_request_duration_sum{api_endpoint="Metrics"} 605.250344830999
|
||||||
|
api_admin_request_duration_count{api_endpoint="Metrics"} 127041
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api_s3_request_counter` (counter)
|
||||||
|
|
||||||
|
Counts the number of requests to a given endpoint of the S3 API. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
api_s3_request_counter{api_endpoint="CreateMultipartUpload"} 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api_s3_error_counter` (counter)
|
||||||
|
|
||||||
|
Counts the number of requests to a given endpoint of the S3 API that returned an error. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
api_s3_error_counter{api_endpoint="GetObject",status_code="404"} 39
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api_s3_request_duration` (histogram)
|
||||||
|
|
||||||
|
Evaluates the duration of API calls to the various S3 API endpoints. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
api_s3_request_duration_bucket{api_endpoint="CreateMultipartUpload",le="0.5"} 1
|
||||||
|
api_s3_request_duration_sum{api_endpoint="CreateMultipartUpload"} 0.046340762
|
||||||
|
api_s3_request_duration_count{api_endpoint="CreateMultipartUpload"} 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api_k2v_request_counter` (counter), `api_k2v_error_counter` (counter), `api_k2v_error_duration` (histogram)
|
||||||
|
|
||||||
|
Same as for S3, for the K2V API.
|
||||||
|
|
||||||
|
|
||||||
|
### Metrics of the Web endpoint
|
||||||
|
|
||||||
|
|
||||||
|
#### `web_request_counter` (counter)
|
||||||
|
|
||||||
|
Number of requests to the web endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
web_request_counter{method="GET"} 80
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `web_request_duration` (histogram)
|
||||||
|
|
||||||
|
Duration of requests to the web endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
web_request_duration_bucket{method="GET",le="0.5"} 80
|
||||||
|
web_request_duration_sum{method="GET"} 1.0528433229999998
|
||||||
|
web_request_duration_count{method="GET"} 80
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `web_error_counter` (counter)
|
||||||
|
|
||||||
|
Number of requests to the web endpoint resulting in errors
|
||||||
|
|
||||||
|
```
|
||||||
|
web_error_counter{method="GET",status_code="404 Not Found"} 64
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Metrics of the data block manager
|
||||||
|
|
||||||
|
#### `block_bytes_read`, `block_bytes_written` (counter)
|
||||||
|
|
||||||
|
Number of bytes read/written to/from disk in the data storage directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
block_bytes_read 120586322022
|
||||||
|
block_bytes_written 3386618077
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `block_read_duration`, `block_write_duration` (histograms)
|
||||||
|
|
||||||
|
Evaluates the duration of the reading/writing of individual data blocks in the data storage directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
block_read_duration_bucket{le="0.5"} 169229
|
||||||
|
block_read_duration_sum 2761.6902550310056
|
||||||
|
block_read_duration_count 169240
|
||||||
|
block_write_duration_bucket{le="0.5"} 3559
|
||||||
|
block_write_duration_sum 195.59170078500006
|
||||||
|
block_write_duration_count 3571
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `block_delete_counter` (counter)
|
||||||
|
|
||||||
|
Counts the number of data blocks that have been deleted from storage.
|
||||||
|
|
||||||
|
```
|
||||||
|
block_delete_counter 122
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `block_resync_counter` (counter), `block_resync_duration` (histogram)
|
||||||
|
|
||||||
|
Counts the number of resync operations the node has executed, and evaluates their duration.
|
||||||
|
|
||||||
|
```
|
||||||
|
block_resync_counter 308897
|
||||||
|
block_resync_duration_bucket{le="0.5"} 308892
|
||||||
|
block_resync_duration_sum 139.64204196100016
|
||||||
|
block_resync_duration_count 308897
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `block_resync_queue_length` (gauge)
|
||||||
|
|
||||||
|
The number of block hashes currently queued for a resync.
|
||||||
|
This is normal to be nonzero for long periods of time.
|
||||||
|
|
||||||
|
```
|
||||||
|
block_resync_queue_length 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `block_resync_errored_blocks` (gauge)
|
||||||
|
|
||||||
|
The number of block hashes that we were unable to resync last time we tried.
|
||||||
|
**THIS SHOULD BE ZERO, OR FALL BACK TO ZERO RAPIDLY, IN A HEALTHY CLUSTER.**
|
||||||
|
Persistent nonzero values indicate that some data is likely to be lost.
|
||||||
|
|
||||||
|
```
|
||||||
|
block_resync_errored_blocks 0
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Metrics related to RPCs (remote procedure calls) between nodes
|
||||||
|
|
||||||
|
#### `rpc_netapp_request_counter` (counter)
|
||||||
|
|
||||||
|
Number of RPC requests emitted
|
||||||
|
|
||||||
|
```
|
||||||
|
rpc_request_counter{from="<this node>",rpc_endpoint="garage_block/manager.rs/Rpc",to="<remote node>"} 176
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `rpc_netapp_error_counter` (counter)
|
||||||
|
|
||||||
|
Number of communication errors (errors in the Netapp library, generally due to disconnected nodes)
|
||||||
|
|
||||||
|
```
|
||||||
|
rpc_netapp_error_counter{from="<this node>",rpc_endpoint="garage_block/manager.rs/Rpc",to="<remote node>"} 354
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `rpc_timeout_counter` (counter)
|
||||||
|
|
||||||
|
Number of RPC timeouts, should be close to zero in a healthy cluster.
|
||||||
|
|
||||||
|
```
|
||||||
|
rpc_timeout_counter{from="<this node>",rpc_endpoint="garage_rpc/membership.rs/SystemRpc",to="<remote node>"} 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `rpc_duration` (histogram)
|
||||||
|
|
||||||
|
The duration of internal RPC calls between Garage nodes.
|
||||||
|
|
||||||
|
```
|
||||||
|
rpc_duration_bucket{from="<this node>",rpc_endpoint="garage_block/manager.rs/Rpc",to="<remote node>",le="0.5"} 166
|
||||||
|
rpc_duration_sum{from="<this node>",rpc_endpoint="garage_block/manager.rs/Rpc",to="<remote node>"} 35.172253716
|
||||||
|
rpc_duration_count{from="<this node>",rpc_endpoint="garage_block/manager.rs/Rpc",to="<remote node>"} 174
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Metrics of the metadata table manager
|
||||||
|
|
||||||
|
#### `table_gc_todo_queue_length` (gauge)
|
||||||
|
|
||||||
|
Table garbage collector TODO queue length
|
||||||
|
|
||||||
|
```
|
||||||
|
table_gc_todo_queue_length{table_name="block_ref"} 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `table_get_request_counter` (counter), `table_get_request_duration` (histogram)
|
||||||
|
|
||||||
|
Number of get/get_range requests internally made on each table, and their duration.
|
||||||
|
|
||||||
|
```
|
||||||
|
table_get_request_counter{table_name="bucket_alias"} 315
|
||||||
|
table_get_request_duration_bucket{table_name="bucket_alias",le="0.5"} 315
|
||||||
|
table_get_request_duration_sum{table_name="bucket_alias"} 0.048509778000000024
|
||||||
|
table_get_request_duration_count{table_name="bucket_alias"} 315
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### `table_put_request_counter` (counter), `table_put_request_duration` (histogram)
|
||||||
|
|
||||||
|
Number of insert/insert_many requests internally made on this table, and their duration
|
||||||
|
|
||||||
|
```
|
||||||
|
table_put_request_counter{table_name="block_ref"} 677
|
||||||
|
table_put_request_duration_bucket{table_name="block_ref",le="0.5"} 677
|
||||||
|
table_put_request_duration_sum{table_name="block_ref"} 61.617528636
|
||||||
|
table_put_request_duration_count{table_name="block_ref"} 677
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `table_internal_delete_counter` (counter)
|
||||||
|
|
||||||
|
Number of value deletions in the tree (due to GC or repartitioning)
|
||||||
|
|
||||||
|
```
|
||||||
|
table_internal_delete_counter{table_name="block_ref"} 2296
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `table_internal_update_counter` (counter)
|
||||||
|
|
||||||
|
Number of value updates where the value actually changes (includes creation of new key and update of existing key)
|
||||||
|
|
||||||
|
```
|
||||||
|
table_internal_update_counter{table_name="block_ref"} 5996
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `table_merkle_updater_todo_queue_length` (gauge)
|
||||||
|
|
||||||
|
Merkle tree updater TODO queue length (should fall to zero rapidly)
|
||||||
|
|
||||||
|
```
|
||||||
|
table_merkle_updater_todo_queue_length{table_name="block_ref"} 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `table_sync_items_received`, `table_sync_items_sent` (counters)
|
||||||
|
|
||||||
|
Number of data items sent to/recieved from other nodes during resync procedures
|
||||||
|
|
||||||
|
```
|
||||||
|
table_sync_items_received{from="<remote node>",table_name="bucket_v2"} 3
|
||||||
|
table_sync_items_sent{table_name="block_ref",to="<remote node>"} 2
|
||||||
|
```
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,9 @@ We recommend first following the [quick start guide](@/documentation/quick-start
|
||||||
to get familiar with Garage's command line and usage patterns.
|
to get familiar with Garage's command line and usage patterns.
|
||||||
|
|
||||||
|
|
||||||
|
## Preparing your environment
|
||||||
|
|
||||||
## Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
To run a real-world deployment, make sure the following conditions are met:
|
To run a real-world deployment, make sure the following conditions are met:
|
||||||
|
|
||||||
|
@ -21,10 +22,6 @@ To run a real-world deployment, make sure the following conditions are met:
|
||||||
- Each machine has a public IP address which is reachable by other machines.
|
- Each machine has a public IP address which is reachable by other machines.
|
||||||
Running behind a NAT is likely to be possible but hasn't been tested for the latest version (TODO).
|
Running behind a NAT is likely to be possible but hasn't been tested for the latest version (TODO).
|
||||||
|
|
||||||
- Ideally, each machine should have a SSD available in addition to the HDD you are dedicating
|
|
||||||
to Garage. This will allow for faster access to metadata and has the potential
|
|
||||||
to significantly reduce Garage's response times.
|
|
||||||
|
|
||||||
- This guide will assume you are using Docker containers to deploy Garage on each node.
|
- This guide will assume you are using Docker containers to deploy Garage on each node.
|
||||||
Garage can also be run independently, for instance as a [Systemd service](@/documentation/cookbook/systemd.md).
|
Garage can also be run independently, for instance as a [Systemd service](@/documentation/cookbook/systemd.md).
|
||||||
You can also use an orchestrator such as Nomad or Kubernetes to automatically manage
|
You can also use an orchestrator such as Nomad or Kubernetes to automatically manage
|
||||||
|
@ -49,6 +46,42 @@ available in the different locations of your cluster is roughly the same.
|
||||||
For instance, here, the Mercury node could be moved to Brussels; this would allow the cluster
|
For instance, here, the Mercury node could be moved to Brussels; this would allow the cluster
|
||||||
to store 2 TB of data in total.
|
to store 2 TB of data in total.
|
||||||
|
|
||||||
|
### Best practices
|
||||||
|
|
||||||
|
- If you have fast dedicated networking between all your nodes, and are planing to store
|
||||||
|
very large files, bump the `block_size` configuration parameter to 10 MB
|
||||||
|
(`block_size = 10485760`).
|
||||||
|
|
||||||
|
- Garage stores its files in two locations: it uses a metadata directory to store frequently-accessed
|
||||||
|
small metadata items, and a data directory to store data blocks of uploaded objects.
|
||||||
|
Ideally, the metadata directory would be stored on an SSD (smaller but faster),
|
||||||
|
and the data directory would be stored on an HDD (larger but slower).
|
||||||
|
|
||||||
|
- For the data directory, Garage already does checksumming and integrity verification,
|
||||||
|
so there is no need to use a filesystem such as BTRFS or ZFS that does it.
|
||||||
|
We recommend using XFS for the data partition, as it has the best performance.
|
||||||
|
EXT4 is not recommended as it has more strict limitations on the number of inodes,
|
||||||
|
which might cause issues with Garage when large numbers of objects are stored.
|
||||||
|
|
||||||
|
- If you only have an HDD and no SSD, it's fine to put your metadata alongside the data
|
||||||
|
on the same drive. Having lots of RAM for your kernel to cache the metadata will
|
||||||
|
help a lot with performance. Make sure to use the LMDB database engine,
|
||||||
|
instead of Sled, which suffers from quite bad performance degradation on HDDs.
|
||||||
|
Sled is still the default for legacy reasons, but is not recommended anymore.
|
||||||
|
|
||||||
|
- For the metadata storage, Garage does not do checksumming and integrity
|
||||||
|
verification on its own. If you are afraid of bitrot/data corruption,
|
||||||
|
put your metadata directory on a BTRFS partition. Otherwise, just use regular
|
||||||
|
EXT4 or XFS.
|
||||||
|
|
||||||
|
- Having a single server with several storage drives is currently not very well
|
||||||
|
supported in Garage ([#218](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/218)).
|
||||||
|
For an easy setup, just put all your drives in a RAID0 or a ZFS RAIDZ array.
|
||||||
|
If you're adventurous, you can try to format each of your disk as
|
||||||
|
a separate XFS partition, and then run one `garage` daemon per disk drive,
|
||||||
|
or use something like [`mergerfs`](https://github.com/trapexit/mergerfs) to merge
|
||||||
|
all your disks in a single union filesystem that spreads load over them.
|
||||||
|
|
||||||
## Get a Docker image
|
## Get a Docker image
|
||||||
|
|
||||||
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
||||||
|
@ -76,11 +109,12 @@ especially you must consider the following folders/files:
|
||||||
this folder will be your main data storage and must be on a large storage (e.g. large HDD)
|
this folder will be your main data storage and must be on a large storage (e.g. large HDD)
|
||||||
|
|
||||||
|
|
||||||
A valid `/etc/garage/garage.toml` for our cluster would look as follows:
|
A valid `/etc/garage.toml` for our cluster would look as follows:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
metadata_dir = "/var/lib/garage/meta"
|
metadata_dir = "/var/lib/garage/meta"
|
||||||
data_dir = "/var/lib/garage/data"
|
data_dir = "/var/lib/garage/data"
|
||||||
|
db_engine = "lmdb"
|
||||||
|
|
||||||
replication_mode = "3"
|
replication_mode = "3"
|
||||||
|
|
||||||
|
@ -90,8 +124,6 @@ rpc_bind_addr = "[::]:3901"
|
||||||
rpc_public_addr = "<this node's public IP>:3901"
|
rpc_public_addr = "<this node's public IP>:3901"
|
||||||
rpc_secret = "<RPC secret>"
|
rpc_secret = "<RPC secret>"
|
||||||
|
|
||||||
bootstrap_peers = []
|
|
||||||
|
|
||||||
[s3_api]
|
[s3_api]
|
||||||
s3_region = "garage"
|
s3_region = "garage"
|
||||||
api_bind_addr = "[::]:3900"
|
api_bind_addr = "[::]:3900"
|
||||||
|
@ -132,6 +164,21 @@ It should be restarted automatically at each reboot.
|
||||||
Please note that we use host networking as otherwise Docker containers
|
Please note that we use host networking as otherwise Docker containers
|
||||||
can not communicate with IPv6.
|
can not communicate with IPv6.
|
||||||
|
|
||||||
|
If you want to use `docker-compose`, you may use the following `docker-compose.yml` file as a reference:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
garage:
|
||||||
|
image: dxflrs/garage:v0.8.0
|
||||||
|
network_mode: "host"
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /etc/garage.toml:/etc/garage.toml
|
||||||
|
- /var/lib/garage/meta:/var/lib/garage/meta
|
||||||
|
- /var/lib/garage/data:/var/lib/garage/data
|
||||||
|
```
|
||||||
|
|
||||||
Upgrading between Garage versions should be supported transparently,
|
Upgrading between Garage versions should be supported transparently,
|
||||||
but please check the relase notes before doing so!
|
but please check the relase notes before doing so!
|
||||||
To upgrade, simply stop and remove this container and
|
To upgrade, simply stop and remove this container and
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
+++
|
+++
|
||||||
title = "Recovering from failures"
|
title = "Recovering from failures"
|
||||||
weight = 35
|
weight = 50
|
||||||
+++
|
+++
|
||||||
|
|
||||||
Garage is meant to work on old, second-hand hardware.
|
Garage is meant to work on old, second-hand hardware.
|
||||||
|
|
|
@ -70,14 +70,16 @@ A possible configuration:
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
upstream s3_backend {
|
upstream s3_backend {
|
||||||
# if you have a garage instance locally
|
# If you have a garage instance locally.
|
||||||
server 127.0.0.1:3900;
|
server 127.0.0.1:3900;
|
||||||
# you can also put your other instances
|
# You can also put your other instances.
|
||||||
server 192.168.1.3:3900;
|
server 192.168.1.3:3900;
|
||||||
# domain names also work
|
# Domain names also work.
|
||||||
server garage1.example.com:3900;
|
server garage1.example.com:3900;
|
||||||
# you can assign weights if you have some servers
|
# A "backup" server is only used if all others have failed.
|
||||||
# that are more powerful than others
|
server garage-remote.example.com:3900 backup;
|
||||||
|
# You can assign weights if you have some servers
|
||||||
|
# that can serve more requests than others.
|
||||||
server garage2.example.com:3900 weight=2;
|
server garage2.example.com:3900 weight=2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +98,8 @@ server {
|
||||||
proxy_pass http://s3_backend;
|
proxy_pass http://s3_backend;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
# Disable buffering to a temporary file.
|
||||||
|
proxy_max_temp_file_size 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
+++
|
+++
|
||||||
title = "Upgrading Garage"
|
title = "Upgrading Garage"
|
||||||
weight = 40
|
weight = 60
|
||||||
+++
|
+++
|
||||||
|
|
||||||
Garage is a stateful clustered application, where all nodes are communicating together and share data structures.
|
Garage is a stateful clustered application, where all nodes are communicating together and share data structures.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
+++
|
+++
|
||||||
title = "Design"
|
title = "Design"
|
||||||
weight = 5
|
weight = 6
|
||||||
sort_by = "weight"
|
sort_by = "weight"
|
||||||
template = "documentation.html"
|
template = "documentation.html"
|
||||||
+++
|
+++
|
||||||
|
|
|
@ -12,7 +12,7 @@ as pictures, video, images, documents, etc., in a redundant multi-node
|
||||||
setting. S3 is versatile enough to also be used to publish a static
|
setting. S3 is versatile enough to also be used to publish a static
|
||||||
website.
|
website.
|
||||||
|
|
||||||
Garage is an opinionated object storage solutoin, we focus on the following **desirable properties**:
|
Garage is an opinionated object storage solution, we focus on the following **desirable properties**:
|
||||||
|
|
||||||
- **Internet enabled**: made for multi-sites (eg. datacenters, offices, households, etc.) interconnected through regular Internet connections.
|
- **Internet enabled**: made for multi-sites (eg. datacenters, offices, households, etc.) interconnected through regular Internet connections.
|
||||||
- **Self-contained & lightweight**: works everywhere and integrates well in existing environments to target [hyperconverged infrastructures](https://en.wikipedia.org/wiki/Hyper-converged_infrastructure).
|
- **Self-contained & lightweight**: works everywhere and integrates well in existing environments to target [hyperconverged infrastructures](https://en.wikipedia.org/wiki/Hyper-converged_infrastructure).
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
+++
|
+++
|
||||||
title = "Development"
|
title = "Development"
|
||||||
weight = 6
|
weight = 7
|
||||||
sort_by = "weight"
|
sort_by = "weight"
|
||||||
template = "documentation.html"
|
template = "documentation.html"
|
||||||
+++
|
+++
|
||||||
|
|
|
@ -42,25 +42,25 @@ you can [build Garage from source](@/documentation/cookbook/from-source.md).
|
||||||
|
|
||||||
## Configuring and starting Garage
|
## Configuring and starting Garage
|
||||||
|
|
||||||
### Writing a first configuration file
|
### Generating a first configuration file
|
||||||
|
|
||||||
This first configuration file should allow you to get started easily with the simplest
|
This first configuration file should allow you to get started easily with the simplest
|
||||||
possible Garage deployment.
|
possible Garage deployment.
|
||||||
**Save it as `/etc/garage.toml`.**
|
|
||||||
You can also store it somewhere else, but you will have to specify `-c path/to/garage.toml`
|
|
||||||
at each invocation of the `garage` binary (for example: `garage -c ./garage.toml server`, `garage -c ./garage.toml status`).
|
|
||||||
|
|
||||||
```toml
|
We will create it with the following command line
|
||||||
|
to generate unique and private secrets for security reasons:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > garage.toml <<EOF
|
||||||
metadata_dir = "/tmp/meta"
|
metadata_dir = "/tmp/meta"
|
||||||
data_dir = "/tmp/data"
|
data_dir = "/tmp/data"
|
||||||
|
db_engine = "lmdb"
|
||||||
|
|
||||||
replication_mode = "none"
|
replication_mode = "none"
|
||||||
|
|
||||||
rpc_bind_addr = "[::]:3901"
|
rpc_bind_addr = "[::]:3901"
|
||||||
rpc_public_addr = "127.0.0.1:3901"
|
rpc_public_addr = "127.0.0.1:3901"
|
||||||
rpc_secret = "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec"
|
rpc_secret = "$(openssl rand -hex 32)"
|
||||||
|
|
||||||
bootstrap_peers = []
|
|
||||||
|
|
||||||
[s3_api]
|
[s3_api]
|
||||||
s3_region = "garage"
|
s3_region = "garage"
|
||||||
|
@ -71,12 +71,26 @@ root_domain = ".s3.garage.localhost"
|
||||||
bind_addr = "[::]:3902"
|
bind_addr = "[::]:3902"
|
||||||
root_domain = ".web.garage.localhost"
|
root_domain = ".web.garage.localhost"
|
||||||
index = "index.html"
|
index = "index.html"
|
||||||
|
|
||||||
|
[k2v_api]
|
||||||
|
api_bind_addr = "[::]:3904"
|
||||||
|
|
||||||
|
[admin]
|
||||||
|
api_bind_addr = "0.0.0.0:3903"
|
||||||
|
admin_token = "$(openssl rand -base64 32)"
|
||||||
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
The `rpc_secret` value provided above is just an example. It will work, but in
|
Now that your configuration file has been created, you can put
|
||||||
order to secure your cluster you will need to use another one. You can generate
|
it in the right place. By default, garage looks at **`/etc/garage.toml`.**
|
||||||
such a value with `openssl rand -hex 32`.
|
|
||||||
|
|
||||||
|
You can also store it somewhere else, but you will have to specify `-c path/to/garage.toml`
|
||||||
|
at each invocation of the `garage` binary (for example: `garage -c ./garage.toml server`, `garage -c ./garage.toml status`).
|
||||||
|
|
||||||
|
As you can see, the `rpc_secret` is a 32 bytes hexadecimal string.
|
||||||
|
You can regenerate it with `openssl rand -hex 32`.
|
||||||
|
If you target a cluster deployment with multiple nodes, make sure that
|
||||||
|
you use the same value for all nodes.
|
||||||
|
|
||||||
As you can see in the `metadata_dir` and `data_dir` parameters, we are saving Garage's data
|
As you can see in the `metadata_dir` and `data_dir` parameters, we are saving Garage's data
|
||||||
in `/tmp` which gets erased when your system reboots. This means that data stored on this
|
in `/tmp` which gets erased when your system reboots. This means that data stored on this
|
||||||
|
@ -219,6 +233,7 @@ Now that we have a bucket and a key, we need to give permissions to the key on t
|
||||||
garage bucket allow \
|
garage bucket allow \
|
||||||
--read \
|
--read \
|
||||||
--write \
|
--write \
|
||||||
|
--owner \
|
||||||
nextcloud-bucket \
|
nextcloud-bucket \
|
||||||
--key nextcloud-app-key
|
--key nextcloud-app-key
|
||||||
```
|
```
|
||||||
|
@ -232,54 +247,73 @@ garage bucket info nextcloud-bucket
|
||||||
|
|
||||||
## Uploading and downlading from Garage
|
## Uploading and downlading from Garage
|
||||||
|
|
||||||
We recommend the use of MinIO Client to interact with Garage files (`mc`).
|
To download and upload files on garage, we can use a third-party tool named `awscli`.
|
||||||
Instructions to install it and use it are provided on the
|
|
||||||
[MinIO website](https://docs.min.io/docs/minio-client-quickstart-guide.html).
|
|
||||||
Before reading the following, you need a working `mc` command on your path.
|
|
||||||
|
|
||||||
Note that on certain Linux distributions such as Arch Linux, the Minio client binary
|
|
||||||
is called `mcli` instead of `mc` (to avoid name clashes with the Midnight Commander).
|
|
||||||
|
|
||||||
### Configure `mc`
|
### Install and configure `awscli`
|
||||||
|
|
||||||
You need your access key and secret key created above.
|
If you have python on your system, you can install it with:
|
||||||
We will assume you are invoking `mc` on the same machine as the Garage server,
|
|
||||||
your S3 API endpoint is therefore `http://127.0.0.1:3900`.
|
|
||||||
For this whole configuration, you must set an alias name: we chose `my-garage`, that you will used for all commands.
|
|
||||||
|
|
||||||
Adapt the following command accordingly and run it:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mc alias set \
|
python -m pip install --user awscli
|
||||||
my-garage \
|
|
||||||
http://127.0.0.1:3900 \
|
|
||||||
<access key> \
|
|
||||||
<secret key> \
|
|
||||||
--api S3v4
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Use `mc`
|
Now that `awscli` is installed, you must configure it to talk to your Garage instance,
|
||||||
|
with your key. There are multiple ways to do that, the simplest one is to create a file
|
||||||
You can not list buckets from `mc` currently.
|
named `~/.awsrc` with this content:
|
||||||
|
|
||||||
But the following commands and many more should work:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mc cp image.png my-garage/nextcloud-bucket
|
export AWS_ACCESS_KEY_ID=xxxx # put your Key ID here
|
||||||
mc cp my-garage/nextcloud-bucket/image.png .
|
export AWS_SECRET_ACCESS_KEY=xxxx # put your Secret key here
|
||||||
mc ls my-garage/nextcloud-bucket
|
export AWS_DEFAULT_REGION='garage'
|
||||||
mc mirror localdir/ my-garage/another-bucket
|
export AWS_ENDPOINT='http://localhost:3900'
|
||||||
|
|
||||||
|
function aws { command aws --endpoint-url $AWS_ENDPOINT $@ ; }
|
||||||
|
aws --version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Now, each time you want to use `awscli` on this target, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ~/.awsrc
|
||||||
|
```
|
||||||
|
|
||||||
|
*You can create multiple files with different names if you
|
||||||
|
have multiple Garage clusters or different keys.
|
||||||
|
Switching from one cluster to another is as simple as
|
||||||
|
sourcing the right file.*
|
||||||
|
|
||||||
|
### Example usage of `awscli`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# list buckets
|
||||||
|
aws s3 ls
|
||||||
|
|
||||||
|
# list objects of a bucket
|
||||||
|
aws s3 ls s3://my_files
|
||||||
|
|
||||||
|
# copy from your filesystem to garage
|
||||||
|
aws s3 cp /proc/cpuinfo s3://my_files/cpuinfo.txt
|
||||||
|
|
||||||
|
# copy from garage to your filesystem
|
||||||
|
aws s3 cp s3/my_files/cpuinfo.txt /tmp/cpuinfo.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that you can use `awscli` for more advanced operations like
|
||||||
|
creating a bucket, pre-signing a request or managing your website.
|
||||||
|
[Read the full documentation to know more](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3/index.html).
|
||||||
|
|
||||||
|
Some features are however not implemented like ACL or policy.
|
||||||
|
Check [our s3 compatibility list](@/documentation/reference-manual/s3-compatibility.md).
|
||||||
|
|
||||||
### Other tools for interacting with Garage
|
### Other tools for interacting with Garage
|
||||||
|
|
||||||
The following tools can also be used to send and recieve files from/to Garage:
|
The following tools can also be used to send and recieve files from/to Garage:
|
||||||
|
|
||||||
- the [AWS CLI](https://aws.amazon.com/cli/)
|
- [minio-client](@/documentation/connect/cli.md#minio-client)
|
||||||
- [`rclone`](https://rclone.org/)
|
- [s3cmd](@/documentation/connect/cli.md#s3cmd)
|
||||||
- [Cyberduck](https://cyberduck.io/)
|
- [rclone](@/documentation/connect/cli.md#rclone)
|
||||||
- [`s3cmd`](https://s3tools.org/s3cmd)
|
- [Cyberduck](@/documentation/connect/cli.md#cyberduck)
|
||||||
|
- [WinSCP](@/documentation/connect/cli.md#winscp)
|
||||||
|
|
||||||
Refer to the ["Integrations" section](@/documentation/connect/_index.md) to learn how to
|
An exhaustive list is maintained in the ["Integrations" > "Browsing tools" section](@/documentation/connect/_index.md).
|
||||||
configure application and command line utilities to integrate with Garage.
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
+++
|
+++
|
||||||
title = "Reference Manual"
|
title = "Reference Manual"
|
||||||
weight = 4
|
weight = 5
|
||||||
sort_by = "weight"
|
sort_by = "weight"
|
||||||
template = "documentation.html"
|
template = "documentation.html"
|
||||||
+++
|
+++
|
||||||
|
|
|
@ -47,598 +47,13 @@ Returns internal Garage metrics in Prometheus format.
|
||||||
|
|
||||||
### Cluster operations
|
### Cluster operations
|
||||||
|
|
||||||
#### GetClusterStatus `GET /v0/status`
|
These endpoints are defined on a dedicated [Redocly page](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.html). You can also download its [OpenAPI specification](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.yml).
|
||||||
|
|
||||||
Returns the cluster's current status in JSON, including:
|
Requesting the API from the command line can be as simple as running:
|
||||||
|
|
||||||
- ID of the node being queried and its version of the Garage daemon
|
```bash
|
||||||
- Live nodes
|
curl -H 'Authorization: Bearer s3cr3t' http://localhost:3903/v0/status | jq
|
||||||
- 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`
|
For more advanced use cases, we recommend using a SDK.
|
||||||
|
[Go to the "Build your own app" section to know how to use our SDKs](@/documentation/build/_index.md)
|
||||||
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": "afa8f0a22b40b1247ccd0affb869b0af5cff980924a20e4b5e0720a44deb8d39",
|
|
||||||
"globalAliases": [],
|
|
||||||
"websiteAccess": false,
|
|
||||||
"websiteConfig": null,
|
|
||||||
"keys": [
|
|
||||||
{
|
|
||||||
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
|
||||||
"name": "Imported key",
|
|
||||||
"permissions": {
|
|
||||||
"read": true,
|
|
||||||
"write": true,
|
|
||||||
"owner": true
|
|
||||||
},
|
|
||||||
"bucketLocalAliases": [
|
|
||||||
"debug"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"objects": 14827,
|
|
||||||
"bytes": 13189855625,
|
|
||||||
"unfinshedUploads": 0,
|
|
||||||
"quotas": {
|
|
||||||
"maxSize": null,
|
|
||||||
"maxObjects": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### CreateBucket `POST /v0/bucket`
|
|
||||||
|
|
||||||
Creates a new storage bucket.
|
|
||||||
|
|
||||||
Request body format:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"globalAlias": "NameOfMyBucket"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
OR
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"localAlias": {
|
|
||||||
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
|
||||||
"alias": "NameOfMyBucket",
|
|
||||||
"allow": {
|
|
||||||
"read": true,
|
|
||||||
"write": true,
|
|
||||||
"owner": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
OR
|
|
||||||
|
|
||||||
```json
|
|
||||||
{}
|
|
||||||
```
|
|
||||||
|
|
||||||
Creates a new bucket, either with a global alias, a local one,
|
|
||||||
or no alias at all.
|
|
||||||
|
|
||||||
Technically, you can also specify both `globalAlias` and `localAlias` and that would create
|
|
||||||
two aliases, but I don't see why you would want to do that.
|
|
||||||
|
|
||||||
#### 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!
|
|
||||||
|
|
||||||
#### UpdateBucket `PUT /v0/bucket?id=<bucket id>`
|
|
||||||
|
|
||||||
Updates configuration of the given bucket.
|
|
||||||
|
|
||||||
Request body format:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"websiteAccess": {
|
|
||||||
"enabled": true,
|
|
||||||
"indexDocument": "index.html",
|
|
||||||
"errorDocument": "404.html"
|
|
||||||
},
|
|
||||||
"quotas": {
|
|
||||||
"maxSize": 19029801,
|
|
||||||
"maxObjects": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
All fields (`websiteAccess` and `quotas`) are optionnal.
|
|
||||||
If they are present, the corresponding modifications are applied to the bucket, otherwise nothing is changed.
|
|
||||||
|
|
||||||
In `websiteAccess`: if `enabled` is `true`, `indexDocument` must be specified.
|
|
||||||
The field `errorDocument` is optional, if no error document is set a generic
|
|
||||||
error message is displayed when errors happen. Conversely, if `enabled` is
|
|
||||||
`false`, neither `indexDocument` nor `errorDocument` must be specified.
|
|
||||||
|
|
||||||
In `quotas`: new values of `maxSize` and `maxObjects` must both be specified, or set to `null`
|
|
||||||
to remove the quotas. An absent value will be considered the same as a `null`. It is not possible
|
|
||||||
to change only one of the two quotas.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ This feature is totally invisible to S3 clients and does not break compatibility
|
||||||
### Cluster administration API
|
### Cluster administration API
|
||||||
|
|
||||||
Garage provides a fully-fledged REST API to administer your cluster programatically.
|
Garage provides a fully-fledged REST API to administer your cluster programatically.
|
||||||
Functionnality included in the admin API include: setting up and monitoring
|
Functionality included in the admin API include: setting up and monitoring
|
||||||
cluster nodes, managing access credentials, and managing storage buckets and bucket aliases.
|
cluster nodes, managing access credentials, and managing storage buckets and bucket aliases.
|
||||||
A full reference of the administration API is available [here](@/documentation/reference-manual/admin-api.md).
|
A full reference of the administration API is available [here](@/documentation/reference-manual/admin-api.md).
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
+++
|
+++
|
||||||
title = "Working Documents"
|
title = "Working Documents"
|
||||||
weight = 7
|
weight = 8
|
||||||
sort_by = "weight"
|
sort_by = "weight"
|
||||||
template = "documentation.html"
|
template = "documentation.html"
|
||||||
+++
|
+++
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
+++
|
+++
|
||||||
title = "Design draft (obsolete)"
|
title = "Design draft (obsolete)"
|
||||||
weight = 50
|
weight = 900
|
||||||
+++
|
+++
|
||||||
|
|
||||||
**WARNING: this documentation is a design draft which was written before Garage's actual implementation.
|
**WARNING: this documentation is a design draft which was written before Garage's actual implementation.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
+++
|
+++
|
||||||
title = "Load balancing data (obsolete)"
|
title = "Load balancing data (obsolete)"
|
||||||
weight = 60
|
weight = 910
|
||||||
+++
|
+++
|
||||||
|
|
||||||
**This is being yet improved in release 0.5. The working document has not been updated yet, it still only applies to Garage 0.2 through 0.4.**
|
**This is being yet improved in release 0.5. The working document has not been updated yet, it still only applies to Garage 0.2 through 0.4.**
|
||||||
|
|
75
doc/book/working-documents/testing-strategy.md
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
+++
|
||||||
|
title = "Testing strategy"
|
||||||
|
weight = 30
|
||||||
|
+++
|
||||||
|
|
||||||
|
|
||||||
|
## Testing Garage
|
||||||
|
|
||||||
|
Currently, we have the following tests:
|
||||||
|
|
||||||
|
- some unit tests spread around the codebase
|
||||||
|
- integration tests written in Rust (`src/garage/test`) to check that Garage operations perform correctly
|
||||||
|
- integration test for compatibility with external tools (`script/test-smoke.sh`)
|
||||||
|
|
||||||
|
We have also tried `minio/mint` but it fails a lot and for now we haven't gotten a lot from it.
|
||||||
|
|
||||||
|
In the future:
|
||||||
|
|
||||||
|
1. We'd like to have a systematic way of testing with `minio/mint`,
|
||||||
|
it would add value to Garage by providing a compatibility score and reference that can be trusted.
|
||||||
|
2. We'd also like to do testing with Jepsen in some way.
|
||||||
|
|
||||||
|
## How to instrument Garagae
|
||||||
|
|
||||||
|
We should try to test in least invasive ways, i.e. minimize the impact of the testing framework on Garage's source code. This means for example:
|
||||||
|
|
||||||
|
- Not abstracting IO/nondeterminism in the source code
|
||||||
|
- Not making `garage` a shared library (launch using `execve`, it's perfectly fine)
|
||||||
|
|
||||||
|
Instead, we should focus on building a clean outer interface for the `garage` binary,
|
||||||
|
for example loading configuration using environnement variables instead of the configuration file if that's helpfull for writing the tests.
|
||||||
|
|
||||||
|
There are two reasons for this:
|
||||||
|
|
||||||
|
- Keep the soure code clean and focused
|
||||||
|
- Test something that is as close as possible as the true garage that will actually be running
|
||||||
|
|
||||||
|
Reminder: rules of simplicity, concerning changes to Garage's source code.
|
||||||
|
Always question what we are doing.
|
||||||
|
Never do anything just because it looks nice or because we "think" it might be usefull at some later point but without knowing precisely why/when.
|
||||||
|
Only do things that make perfect sense in the context of what we currently know.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
Testing is a research field on its own.
|
||||||
|
About testing distributed systems:
|
||||||
|
|
||||||
|
- [Jepsen](https://jepsen.io/) is a testing framework designed to test distributed systems. It can mock some part of the system like the time and the network.
|
||||||
|
- [FoundationDB Testing Approach](https://www.micahlerner.com/2021/06/12/foundationdb-a-distributed-unbundled-transactional-key-value-store.html#what-is-unique-about-foundationdbs-testing-framework). They chose to abstract "all sources of nondeterminism and communication are abstracted, including network, disk, time, and pseudo random number generator" to be able to run tests by simulating faults.
|
||||||
|
- [Testing Distributed Systems](https://asatarin.github.io/testing-distributed-systems/) - Curated list of resources on testing distributed systems
|
||||||
|
|
||||||
|
About S3 compatibility:
|
||||||
|
- [ceph/s3-tests](https://github.com/ceph/s3-tests)
|
||||||
|
- (deprecated) [minio/s3verify](https://blog.min.io/s3verify-a-simple-tool-to-verify-aws-s3-api-compatibility/)
|
||||||
|
- [minio/mint](https://github.com/minio/mint)
|
||||||
|
|
||||||
|
About benchmarking S3 (I think it is not necessarily very relevant for this iteration):
|
||||||
|
- [minio/warp](https://github.com/minio/warp)
|
||||||
|
- [wasabi-tech/s3-benchmark](https://github.com/wasabi-tech/s3-benchmark)
|
||||||
|
- [dvassallo/s3-benchmark](https://github.com/dvassallo/s3-benchmark)
|
||||||
|
- [intel-cloud/cosbench](https://github.com/intel-cloud/cosbench) - used by Ceph
|
||||||
|
|
||||||
|
Engineering blog posts:
|
||||||
|
- [Quincy @ Scale: A Tale of Three Large-Scale Clusters](https://ceph.io/en/news/blog/2022/three-large-scale-clusters/)
|
||||||
|
|
||||||
|
Interesting blog posts on the blog of the Sled database:
|
||||||
|
|
||||||
|
- <https://sled.rs/simulation.html>
|
||||||
|
- <https://sled.rs/perf.html>
|
||||||
|
|
||||||
|
Misc:
|
||||||
|
- [mutagen](https://github.com/llogiq/mutagen) - mutation testing is a way to assert our test quality by mutating the code and see if the mutation makes the tests fail
|
||||||
|
- [fuzzing](https://rust-fuzz.github.io/book/) - cargo supports fuzzing, it could be a way to test our software reliability in presence of garbage data.
|
||||||
|
|
||||||
|
|
686
doc/drafts/admin-api.md
Normal file
|
@ -0,0 +1,686 @@
|
||||||
|
+++
|
||||||
|
title = "Administration API"
|
||||||
|
weight = 60
|
||||||
|
+++
|
||||||
|
|
||||||
|
The Garage administration API is accessible through a dedicated server whose
|
||||||
|
listen address is specified in the `[admin]` section of the configuration
|
||||||
|
file (see [configuration file
|
||||||
|
reference](@/documentation/reference-manual/configuration.md))
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
The Garage administration API was introduced in version 0.7.2, this document
|
||||||
|
does not apply to older versions of Garage.
|
||||||
|
|
||||||
|
|
||||||
|
## 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, access to these
|
||||||
|
endpoints is disabled entirely).
|
||||||
|
|
||||||
|
These tokens are used as simple HTTP bearer tokens. In other words, to
|
||||||
|
authenticate access to an admin API endpoint, add the following HTTP header
|
||||||
|
to your request:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Administration API endpoints
|
||||||
|
|
||||||
|
### Metrics-related endpoints
|
||||||
|
|
||||||
|
#### Metrics `GET /metrics`
|
||||||
|
|
||||||
|
Returns internal Garage metrics in Prometheus format.
|
||||||
|
|
||||||
|
#### Health `GET /health`
|
||||||
|
|
||||||
|
Used for simple health checks in a cluster setting with an orchestrator.
|
||||||
|
Returns an HTTP status 200 if the node is ready to answer user's requests,
|
||||||
|
and an HTTP status 503 (Service Unavailable) if there are some partitions
|
||||||
|
for which a quorum of nodes is not available.
|
||||||
|
A simple textual message is also returned in a body with content-type `text/plain`.
|
||||||
|
See `/v0/health` for an API that also returns JSON output.
|
||||||
|
|
||||||
|
### 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GetClusterHealth `GET /v0/health`
|
||||||
|
|
||||||
|
Returns the cluster's current health in JSON format, with the following variables:
|
||||||
|
|
||||||
|
- `status`: one of `Healthy`, `Degraded` or `Unavailable`:
|
||||||
|
- Healthy: Garage node is connected to all storage nodes
|
||||||
|
- Degraded: Garage node is not connected to all storage nodes, but a quorum of write nodes is available for all partitions
|
||||||
|
- Unavailable: a quorum of write nodes is not available for some partitions
|
||||||
|
- `known_nodes`: the number of nodes this Garage node has had a TCP connection to since the daemon started
|
||||||
|
- `connected_nodes`: the nubmer of nodes this Garage node currently has an open connection to
|
||||||
|
- `storage_nodes`: the number of storage nodes currently registered in the cluster layout
|
||||||
|
- `storage_nodes_ok`: the number of storage nodes to which a connection is currently open
|
||||||
|
- `partitions`: the total number of partitions of the data (currently always 256)
|
||||||
|
- `partitions_quorum`: the number of partitions for which a quorum of write nodes is available
|
||||||
|
- `partitions_all_ok`: the number of partitions for which we are connected to all storage nodes responsible of storing it
|
||||||
|
|
||||||
|
Contrarily to `GET /health`, this endpoint always returns a 200 OK HTTP response code.
|
||||||
|
|
||||||
|
Example response body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "Degraded",
|
||||||
|
"known_nodes": 3,
|
||||||
|
"connected_nodes": 2,
|
||||||
|
"storage_nodes": 3,
|
||||||
|
"storage_nodes_ok": 2,
|
||||||
|
"partitions": 256,
|
||||||
|
"partitions_quorum": 256,
|
||||||
|
"partitions_all_ok": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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": "afa8f0a22b40b1247ccd0affb869b0af5cff980924a20e4b5e0720a44deb8d39",
|
||||||
|
"globalAliases": [],
|
||||||
|
"websiteAccess": false,
|
||||||
|
"websiteConfig": null,
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"name": "Imported key",
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": true
|
||||||
|
},
|
||||||
|
"bucketLocalAliases": [
|
||||||
|
"debug"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"objects": 14827,
|
||||||
|
"bytes": 13189855625,
|
||||||
|
"unfinshedUploads": 0,
|
||||||
|
"quotas": {
|
||||||
|
"maxSize": null,
|
||||||
|
"maxObjects": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CreateBucket `POST /v0/bucket`
|
||||||
|
|
||||||
|
Creates a new storage bucket.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"globalAlias": "NameOfMyBucket"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
OR
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"localAlias": {
|
||||||
|
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||||
|
"alias": "NameOfMyBucket",
|
||||||
|
"allow": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"owner": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
OR
|
||||||
|
|
||||||
|
```json
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a new bucket, either with a global alias, a local one,
|
||||||
|
or no alias at all.
|
||||||
|
|
||||||
|
Technically, you can also specify both `globalAlias` and `localAlias` and that would create
|
||||||
|
two aliases, but I don't see why you would want to do that.
|
||||||
|
|
||||||
|
#### 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!
|
||||||
|
|
||||||
|
#### UpdateBucket `PUT /v0/bucket?id=<bucket id>`
|
||||||
|
|
||||||
|
Updates configuration of the given bucket.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"websiteAccess": {
|
||||||
|
"enabled": true,
|
||||||
|
"indexDocument": "index.html",
|
||||||
|
"errorDocument": "404.html"
|
||||||
|
},
|
||||||
|
"quotas": {
|
||||||
|
"maxSize": 19029801,
|
||||||
|
"maxObjects": null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All fields (`websiteAccess` and `quotas`) are optionnal.
|
||||||
|
If they are present, the corresponding modifications are applied to the bucket, otherwise nothing is changed.
|
||||||
|
|
||||||
|
In `websiteAccess`: if `enabled` is `true`, `indexDocument` must be specified.
|
||||||
|
The field `errorDocument` is optional, if no error document is set a generic
|
||||||
|
error message is displayed when errors happen. Conversely, if `enabled` is
|
||||||
|
`false`, neither `indexDocument` nor `errorDocument` must be specified.
|
||||||
|
|
||||||
|
In `quotas`: new values of `maxSize` and `maxObjects` must both be specified, or set to `null`
|
||||||
|
to remove the quotas. An absent value will be considered the same as a `null`. It is not possible
|
||||||
|
to change only one of the two quotas.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
10
doc/talks/2022-11-19-Capitole-du-Libre/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
*.aux
|
||||||
|
*.bbl
|
||||||
|
*.blg
|
||||||
|
*.log
|
||||||
|
*.nav
|
||||||
|
*.out
|
||||||
|
*.snm
|
||||||
|
*.synctex.gz
|
||||||
|
*.toc
|
||||||
|
*.dvi
|
8
doc/talks/2022-11-19-Capitole-du-Libre/Makefile
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
all:
|
||||||
|
pdflatex présentation.tex
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f *.aux *.bbl *.blg *.log *.nav *.out *.snm *.synctex.gz *.toc *.dvi présentation.pdf
|
||||||
|
|
||||||
|
clean_sauf_pdf:
|
||||||
|
rm -f *.aux *.bbl *.blg *.log *.nav *.out *.snm *.synctex.gz *.toc *.dvi
|
BIN
doc/talks/2022-11-19-Capitole-du-Libre/NGI.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/agpl-v3-logo.png
Normal file
After Width: | Height: | Size: 196 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/carte-Europe.pdf
Normal file
BIN
doc/talks/2022-11-19-Capitole-du-Libre/deuxfleurs-logo.png
Normal file
After Width: | Height: | Size: 105 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/drapeau_européen.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/garage-logo.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/logo_chatons.png
Normal file
After Width: | Height: | Size: 199 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/mastodon-logo.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/matrix-logo.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/neptune.jpg
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/nextcloud-logo.png
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/peertube-logo.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/présentation.pdf
Normal file
340
doc/talks/2022-11-19-Capitole-du-Libre/présentation.tex
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
\documentclass[11pt, aspectratio=1610]{beamer}
|
||||||
|
\usetheme{Warsaw}
|
||||||
|
\usepackage[utf8]{inputenc}
|
||||||
|
\usepackage[french]{babel}
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage{amsfonts}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
\usepackage{tikz}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage{xcolor}
|
||||||
|
\usepackage{setspace}
|
||||||
|
\usepackage{todonotes}
|
||||||
|
\presetkeys{todonotes}{inline}{}
|
||||||
|
\renewcommand{\baselinestretch}{1.25}
|
||||||
|
|
||||||
|
\definecolor{orange_garage}{RGB}{255,147,41}
|
||||||
|
\definecolor{gris_garage}{RGB}{78,78,78}
|
||||||
|
|
||||||
|
\author[Association Deuxfleurs]{~\linebreak Vincent Giraud}
|
||||||
|
\title[De l'auto-hébergement à l'entre-hébergement avec Garage]{De l'auto-hébergement à l'entre-hébergement :\\Garage, pour conserver ses données ensemble}
|
||||||
|
%\setbeamercovered{transparent}
|
||||||
|
%\setbeamertemplate{navigation symbols}{}
|
||||||
|
\date{Capitole du Libre 2022\linebreak
|
||||||
|
|
||||||
|
\scriptsize Samedi 19 novembre 2022\linebreak
|
||||||
|
}
|
||||||
|
|
||||||
|
\setbeamercolor{palette primary}{fg=gris_garage,bg=orange_garage}
|
||||||
|
\setbeamercolor{palette secondary}{fg=gris_garage,bg=gris_garage}
|
||||||
|
\setbeamercolor{palette tiertary}{fg=white,bg=gris_garage}
|
||||||
|
\setbeamercolor{palette quaternary}{fg=white,bg=gris_garage}
|
||||||
|
\setbeamercolor{navigation symbols}{fg=black, bg=white}
|
||||||
|
\setbeamercolor{navigation symbols dimmed}{fg=darkgray, bg=white}
|
||||||
|
\setbeamercolor{itemize item}{fg=gris_garage}
|
||||||
|
\setbeamertemplate{itemize item}[circle]
|
||||||
|
|
||||||
|
\addtobeamertemplate{navigation symbols}{}{%
|
||||||
|
\usebeamerfont{footline}%
|
||||||
|
\usebeamercolor[fg]{footline}%
|
||||||
|
\hspace{1em}%
|
||||||
|
\insertframenumber/\inserttotalframenumber
|
||||||
|
}
|
||||||
|
|
||||||
|
\setbeamertemplate{headline}
|
||||||
|
{%
|
||||||
|
\leavevmode%
|
||||||
|
\begin{beamercolorbox}[wd=.5\paperwidth,ht=2.5ex,dp=1.125ex]{section in head/foot}%
|
||||||
|
\hbox to .5\paperwidth{\hfil\insertsectionhead\hfil}
|
||||||
|
\end{beamercolorbox}%
|
||||||
|
\begin{beamercolorbox}[wd=.5\paperwidth,ht=2.5ex,dp=1.125ex]{subsection in head/foot}%
|
||||||
|
\hbox to .5\paperwidth{\hfil\insertsubsectionhead\hfil}
|
||||||
|
\end{beamercolorbox}%
|
||||||
|
}
|
||||||
|
\addtobeamertemplate{footnote}{}{\vspace{2ex}}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\begin{frame}
|
||||||
|
\titlepage
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\section{Introduction}
|
||||||
|
\subsection{Présentation}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{columns}
|
||||||
|
\column{0.5 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
\includegraphics[width=3.5cm]{deuxfleurs-logo.png}\linebreak
|
||||||
|
|
||||||
|
\texttt{https://deuxfleurs.fr}
|
||||||
|
\end{center}
|
||||||
|
\column{0.4 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
Deuxfleurs est une association militant en faveur d'un internet plus convivial, avec une organisation et des rapports de force repensés.\linebreak
|
||||||
|
|
||||||
|
Nous faisons partie du CHATONS\footnote[frame]{Collectif des Hébergeurs Alternatifs, Transparents, Ouverts, Neutres et Solidaires} depuis avril 2022.
|
||||||
|
|
||||||
|
\includegraphics[width=2cm]{logo_chatons.png}
|
||||||
|
\end{center}
|
||||||
|
\end{columns}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Héberger à la maison}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{columns}
|
||||||
|
\begin{column}{0.5 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
Pour échapper au contrôle et au giron des opérateurs de clouds, héberger ses données à la maison présente de nombreux avantages...
|
||||||
|
\end{center}
|
||||||
|
|
||||||
|
\vspace{0.5cm}
|
||||||
|
|
||||||
|
\begin{itemize}[<+(1)->]
|
||||||
|
\item On récupère la souveraineté sur ses données
|
||||||
|
\item On gagne en vie privée
|
||||||
|
\item On gagne en libertés
|
||||||
|
\item On est responsabilisé face à ses besoins
|
||||||
|
\end{itemize}
|
||||||
|
\end{column}
|
||||||
|
\vrule{}
|
||||||
|
\begin{column}{0.5 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
\onslide<6->{... mais aussi bien des contraintes...}
|
||||||
|
\end{center}
|
||||||
|
|
||||||
|
\vspace{0.5cm}
|
||||||
|
|
||||||
|
\begin{itemize}[<+(2)->]
|
||||||
|
\item On repose sur une connexion internet pour particulier
|
||||||
|
\item Un certain savoir-faire et moultes compétences sont requis
|
||||||
|
\item Assurer la résilience de ses services est difficile
|
||||||
|
\item Bien sauvegarder ses données, et ceci au-delà de son site géographique, n'est pas évident
|
||||||
|
\end{itemize}
|
||||||
|
\end{column}
|
||||||
|
\end{columns}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Sauvegarder pour se parer à tout imprévu}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
Sauvegarder pour se parer contre les pannes matérielles est une chose...
|
||||||
|
|
||||||
|
Sauvegarder pour se parer contre les cambriolages et les incendies en est une autre !\linebreak
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
\onslide<2->{Répartir géographiquement ses données devient alors nécessaire.}
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\section{Les solutions à explorer}
|
||||||
|
\subsection{L'entre-hébergement}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
On a vu récemment se développer au sein du CHATONS la notion d'entre-hébergement : en plus de renforcer l'intégrité des sauvegardes, on va améliorer la disponibilité pendant les coupures de liaison internet, de courant, ou pendant les déménagements d'administrateurs par exemple.\linebreak
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
\onslide<2->
|
||||||
|
{
|
||||||
|
Dans le cadre du collectif, il s'agit de partager ses volumes de données entre hébergeurs.\linebreak
|
||||||
|
|
||||||
|
Pour assurer la confidentialité, on peut chiffrer les données au niveau applicatif.
|
||||||
|
}
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{S3 contre les systèmes de fichiers}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
Dans le cadre de l'administration de services en ligne, les systèmes de fichiers recèlent certaines difficultés.\linebreak
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
Le standard S3 apporte des facilités; on réduit le stockage à un paradigme de clé-valeur basé essentiellement sur deux opérations seulement: lire ou écrire une clé.
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\section{Garage}
|
||||||
|
\subsection{Présentation}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{columns}
|
||||||
|
\column{0.5 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
Garage essaye de répondre à l'ensemble de ces besoins.\linebreak
|
||||||
|
|
||||||
|
\vspace{0.5cm}
|
||||||
|
Il s'agit d'un logiciel libre permettant de distribuer un service S3 sur diverses machines éloignées.
|
||||||
|
\end{center}
|
||||||
|
\column{0.5 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
\includegraphics[width=4cm]{garage-logo.png}\linebreak
|
||||||
|
|
||||||
|
\texttt{https://garagehq.deuxfleurs.fr/}
|
||||||
|
\end{center}
|
||||||
|
\end{columns}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Gestion des zones}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
Garage va prendre en compte les zones géographiques au moment de répliquer les données.\linebreak
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
\includegraphics[width=13.25cm]{zones.png}
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Comment ça marche ?}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{columns}
|
||||||
|
\column{0.5 \linewidth}
|
||||||
|
\input{schéma europe}
|
||||||
|
\column{0.5 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
Chaque objet est dupliqué sur plusieurs zones différentes.\linebreak
|
||||||
|
|
||||||
|
\onslide<5->{Lorsqu'un nouvel hébergeur rejoint le réseau, la charge se voit équilibrée.}\linebreak
|
||||||
|
|
||||||
|
\onslide<12->{Si une zone devient indisponible, les autres continuent d'assurer le service.}\linebreak
|
||||||
|
\end{center}
|
||||||
|
\end{columns}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Financement}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
Dans le cadre du programme \textit{Horizon 2021} de l'Union Européenne, nous avons reçu une subvention de la part de l'initiative NGI Pointer\footnote[frame]{Next Generation Internet Program for Open Internet Renovation}.\linebreak
|
||||||
|
|
||||||
|
\includegraphics[width=3cm]{drapeau_européen.png}\hspace{1cm}
|
||||||
|
\includegraphics[width=3cm]{NGI.png}\linebreak
|
||||||
|
|
||||||
|
Nous avons ainsi pu financer le développement de Garage pendant 1 an.
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Licence}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
De par nos valeurs, nous avons attribué la licence AGPL version 3 à Garage, notamment afin qu'il reste parmi les biens communs.\linebreak
|
||||||
|
|
||||||
|
\vspace{0.5cm}
|
||||||
|
\includegraphics[width=5cm]{agpl-v3-logo.png}\linebreak
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Langage utilisé}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
Nous avons décidé d'écrire Garage à l'aide du langage Rust, afin d'obtenir une compilation vers des binaires natifs et efficaces.\linebreak
|
||||||
|
|
||||||
|
\includegraphics[width=3.5cm]{rust-logo.png}\linebreak
|
||||||
|
|
||||||
|
Ce choix permet également de bénéficier des avantages reconnus de Rust en termes de sécurité.
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Matériel requis}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
Garage peut ainsi être performant sur des machines limitées. Les prérequis sont minimes : n'importe quelle machine avec un processeur qui a moins d'une décennie, 1~gigaoctet de mémoire vive, et 16~gigaoctets de stockage suffit.\linebreak
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
|
||||||
|
Cet aspect est déterminant : il permet en effet d'héberger sur du matériel acheté d'occasion, pour réduire l'impact écologique de nos infrastructures.
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Performances}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
\includegraphics[width=13.25cm]{rpc-amplification.png}
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
\includegraphics[width=11cm]{rpc-complexity.png}
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Services}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
Puisqu'il suit le standard S3, beaucoup de services populaires sont par conséquence compatibles avec Garage :\linebreak
|
||||||
|
|
||||||
|
\begin{columns}
|
||||||
|
\column{0.2 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
\includegraphics[width=2.5cm]{nextcloud-logo.png}
|
||||||
|
\end{center}
|
||||||
|
\column{0.2 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
\includegraphics[width=2.5cm]{peertube-logo.png}
|
||||||
|
\end{center}
|
||||||
|
\column{0.2 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
\includegraphics[width=2.5cm]{matrix-logo.png}
|
||||||
|
\end{center}
|
||||||
|
\column{0.2 \linewidth}
|
||||||
|
\begin{center}
|
||||||
|
\includegraphics[width=2.5cm]{mastodon-logo.png}
|
||||||
|
\end{center}
|
||||||
|
\end{columns}
|
||||||
|
~\linebreak
|
||||||
|
|
||||||
|
Et comme souvent avec S3, on peut assimiler un bucket à un site, et utiliser le serveur pour héberger des sites web statiques.
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\section{Intégration chez Deuxfleurs}
|
||||||
|
\subsection{Matériel}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
\includegraphics[width=13cm]{neptune.jpg}\linebreak
|
||||||
|
|
||||||
|
En pratique, nos serveurs ne sont effectivement que des machines achetées d'occasion (très souvent des anciens ordinateurs destinés à la bureautique en entreprise).
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{Environnement logiciel}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
Pour faciliter la reproduction d'un environnement connu, NixOS est installé sur nos machines.\linebreak
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
Pour s’accommoder des réseaux qu'on trouve derrière des routeurs pour particuliers, on s'aide de notre logiciel Diplonat\footnote[frame]{\texttt{https://git.deuxfleurs.fr/Deuxfleurs/diplonat}}.
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\section{Au-delà...}
|
||||||
|
\subsection{... de Deuxfleurs}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
\includegraphics[width=10cm]{tedomum.png}
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\subsection{... de Garage}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
Nous avons récemment lancé le développement d'Aérogramme\footnote[frame]{\texttt{https://git.deuxfleurs.fr/Deuxfleurs/aerogramme}}.\linebreak
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
Il s'agit d'un serveur de stockage de courriels chiffrés.\linebreak
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
Il est conçu pour pouvoir travailler avec Garage.
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\section{Fin}
|
||||||
|
\subsection{Contacts}
|
||||||
|
\begin{frame}
|
||||||
|
\begin{center}
|
||||||
|
\begin{tikzpicture}
|
||||||
|
\node (ronce) {\includegraphics[width=0.95\textwidth]{ronce.jpg}};
|
||||||
|
\node[white] at (-5.1,3.6) {Intéressé(e) ?};
|
||||||
|
\node[white, align=center] at (4.2,-2.6) {Contactez-nous !\\\texttt{coucou@deuxfleurs.fr}\\\texttt{\#forum:deuxfleurs.fr}};
|
||||||
|
\end{tikzpicture}
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
\end{document}
|
||||||
|
|
BIN
doc/talks/2022-11-19-Capitole-du-Libre/ronce.jpg
Normal file
After Width: | Height: | Size: 1.4 MiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/rpc-amplification.png
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/rpc-complexity.png
Normal file
After Width: | Height: | Size: 194 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/rust-logo.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
52
doc/talks/2022-11-19-Capitole-du-Libre/schéma europe.tex
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
\begin{tikzpicture}
|
||||||
|
\node (carte) {\includegraphics[width=\textwidth]{carte-Europe.pdf}};
|
||||||
|
|
||||||
|
% \personnage{position X}{position Y}{facteur d'échelle}
|
||||||
|
\newcommand{\personnage}[4]
|
||||||
|
{
|
||||||
|
\fill[#4] ({#1-(0.4 * #3)},{#2-(0.9 * #3)}) .. controls ({#1-(0.4 * #3)},#2) and ({#1+(0.4 * #3)},#2) .. ({#1+(0.4 * #3)},{#2-(0.9 * #3)}) -- ({#1-(0.4 * #3)},{#2-(0.9 * #3)});
|
||||||
|
\fill[#4] (#1,#2) circle ({0.25 * #3});
|
||||||
|
}
|
||||||
|
|
||||||
|
\onslide<1-11>{\personnage{-2.25}{-0.75}{0.75}{green}}
|
||||||
|
\onslide<1-11>{\draw (-1.9,-1.6) rectangle ++(1,1.2);}
|
||||||
|
\onslide<2-11>{\draw[fill=green] (-1.8,-1.525) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 1};}
|
||||||
|
\onslide<4-5>{\draw[fill=red] (-1.8,-1.15) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 3};}
|
||||||
|
\onslide<7-11>{\draw[fill=yellow] (-1.8,-1.15) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 4};}
|
||||||
|
\onslide<9-11>{\draw[fill=red] (-1.8,-0.775) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 6};}
|
||||||
|
\onslide<3-11>{\draw[fill=blue] (-1.35,-1.525) rectangle ++(0.35,0.3) node[pos=0.5, white] {\tiny 2};}
|
||||||
|
\onslide<8-11>{\draw[fill=blue] (-1.35,-1.15) rectangle ++(0.35,0.3) node[pos=0.5, white] {\tiny 5};}
|
||||||
|
\onslide<11-11>{\draw[fill=yellow] (-1.35,-0.775) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 8};}
|
||||||
|
|
||||||
|
\personnage{1.65}{1.5}{0.75}{blue}
|
||||||
|
\draw (0.3,0.7) rectangle ++(1,1.2);
|
||||||
|
\onslide<2->{\draw[fill=green] (0.4,0.775) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 1};}
|
||||||
|
\onslide<4->{\draw[fill=red] (0.4,1.15) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 3};}
|
||||||
|
\onslide<10->{\draw[fill=green] (0.4,1.525) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 7};}
|
||||||
|
\onslide<3->{\draw[fill=blue] (0.85,0.775) rectangle ++(0.35,0.3) node[pos=0.5, white] {\tiny 2};}
|
||||||
|
\onslide<9->{\draw[fill=red] (0.85,1.15) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 6};}
|
||||||
|
\onslide<11->{\draw[fill=yellow] (0.85,1.525) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 8};}
|
||||||
|
|
||||||
|
\personnage{1.85}{-2.3}{0.75}{red}
|
||||||
|
\draw (0.5,-3.15) rectangle ++(1,1.2);
|
||||||
|
\onslide<2->{\draw[fill=green] (0.6,-3.075) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 1};}
|
||||||
|
\onslide<4-5>{\draw[fill=red] (0.6,-2.7) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 3};}
|
||||||
|
\onslide<7->{\draw[fill=yellow] (0.6,-2.7) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 4};}
|
||||||
|
\onslide<9->{\draw[fill=red] (0.6,-2.325) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 6};}
|
||||||
|
\onslide<3-5>{\draw[fill=blue] (1.05,-3.075) rectangle ++(0.35,0.3) node[pos=0.5, white] {\tiny 2};}
|
||||||
|
\onslide<6->{\draw[fill=red] (1.05,-3.075) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 3};}
|
||||||
|
\onslide<8->{\draw[fill=blue] (1.05,-2.7) rectangle ++(0.35,0.3) node[pos=0.5, white] {\tiny 5};}
|
||||||
|
\onslide<10->{\draw[fill=green] (1.05,-2.325) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 7};}
|
||||||
|
|
||||||
|
\onslide<5->{\personnage{1.05}{-0.15}{0.75}{yellow}}
|
||||||
|
\onslide<5->{\draw (-0.35,-1) rectangle ++(1,1.2);}
|
||||||
|
\onslide<6->{\draw[fill=blue] (-0.25,-0.925) rectangle ++(0.35,0.3) node[pos=0.5, white] {\tiny 2};}
|
||||||
|
\onslide<7->{\draw[fill=yellow] (-0.25,-0.55) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 4};}
|
||||||
|
\onslide<10->{\draw[fill=green] (-0.25,-0.175) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 7};}
|
||||||
|
\onslide<6->{\draw[fill=red] (0.2,-0.925) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 3};}
|
||||||
|
\onslide<8->{\draw[fill=blue] (0.2,-0.55) rectangle ++(0.35,0.3) node[pos=0.5,white] {\tiny 5};}
|
||||||
|
\onslide<11->{\draw[fill=yellow] (0.2,-0.175) rectangle ++(0.35,0.3) node[pos=0.5] {\tiny 8};}
|
||||||
|
|
||||||
|
\onslide<12->{\draw[line width=0.25cm] (-2.15,-0.5) -- ++(1,-1);}
|
||||||
|
\onslide<12->{\draw[line width=0.25cm] (-2.15,-1.5) -- ++(1,1);}
|
||||||
|
\end{tikzpicture}
|
BIN
doc/talks/2022-11-19-Capitole-du-Libre/tedomum.png
Normal file
After Width: | Height: | Size: 236 KiB |
BIN
doc/talks/2022-11-19-Capitole-du-Libre/zones.png
Normal file
After Width: | Height: | Size: 97 KiB |
108
flake.lock
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"cargo2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1666087781,
|
||||||
|
"narHash": "sha256-trKVdjMZ8mNkGfLcY5LsJJGtdV3xJDZnMVrkFjErlcs=",
|
||||||
|
"owner": "Alexis211",
|
||||||
|
"repo": "cargo2nix",
|
||||||
|
"rev": "a7a61179b66054904ef6a195d8da736eaaa06c36",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "Alexis211",
|
||||||
|
"repo": "cargo2nix",
|
||||||
|
"rev": "a7a61179b66054904ef6a195d8da736eaaa06c36",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1650374568,
|
||||||
|
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1659877975,
|
||||||
|
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1665657542,
|
||||||
|
"narHash": "sha256-mojxNyzbvmp8NtVtxqiHGhRfjCALLfk9i/Uup68Y5q8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a3073c49bc0163fea6a121c276f526837672b555",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a3073c49bc0163fea6a121c276f526837672b555",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"cargo2nix": "cargo2nix",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"cargo2nix",
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"cargo2nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1664247556,
|
||||||
|
"narHash": "sha256-J4vazHU3609ekn7dr+3wfqPo5WGlZVAgV7jfux352L0=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "524db9c9ea7bc7743bb74cdd45b6d46ea3fcc2ab",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
28
flake.nix
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
description = "Garage, an S3-compatible distributed object store for self-hosted deployments";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/a3073c49bc0163fea6a121c276f526837672b555";
|
||||||
|
inputs.cargo2nix = {
|
||||||
|
# As of 2022-10-18: two small patches over unstable branch, one for clippy and one to fix feature detection
|
||||||
|
url = "github:Alexis211/cargo2nix/a7a61179b66054904ef6a195d8da736eaaa06c36";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, cargo2nix }: let
|
||||||
|
git_version = self.lastModifiedDate;
|
||||||
|
compile = import ./nix/compile.nix;
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems (system: {
|
||||||
|
default = (compile {
|
||||||
|
inherit system git_version;
|
||||||
|
pkgsSrc = nixpkgs;
|
||||||
|
cargo2nixOverlay = cargo2nix.overlays.default;
|
||||||
|
release = true;
|
||||||
|
}).workspace.garage {
|
||||||
|
compileMode = "build";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,24 +1,31 @@
|
||||||
{
|
{
|
||||||
system ? builtins.currentSystem,
|
system,
|
||||||
target,
|
target ? null,
|
||||||
|
pkgsSrc,
|
||||||
|
cargo2nixOverlay,
|
||||||
compiler ? "rustc",
|
compiler ? "rustc",
|
||||||
release ? false,
|
release ? false,
|
||||||
git_version ? null,
|
git_version ? null,
|
||||||
features ? null,
|
features ? null,
|
||||||
}:
|
}:
|
||||||
|
|
||||||
with import ./common.nix;
|
|
||||||
|
|
||||||
let
|
let
|
||||||
log = v: builtins.trace v v;
|
log = v: builtins.trace v v;
|
||||||
|
|
||||||
pkgs = import pkgsSrc {
|
pkgs =
|
||||||
|
if target != null then
|
||||||
|
import pkgsSrc {
|
||||||
inherit system;
|
inherit system;
|
||||||
crossSystem = {
|
crossSystem = {
|
||||||
config = target;
|
config = target;
|
||||||
isStatic = true;
|
isStatic = true;
|
||||||
};
|
};
|
||||||
overlays = [ cargo2nixOverlay ];
|
overlays = [ cargo2nixOverlay ];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
import pkgsSrc {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ cargo2nixOverlay ];
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -34,7 +41,7 @@ let
|
||||||
NixOS ships them in separate ones. We reunite them with symlinkJoin.
|
NixOS ships them in separate ones. We reunite them with symlinkJoin.
|
||||||
*/
|
*/
|
||||||
toolchainOptions =
|
toolchainOptions =
|
||||||
if target == "x86_64-unknown-linux-musl" || target == "aarch64-unknown-linux-musl" then {
|
if target == null || target == "x86_64-unknown-linux-musl" || target == "aarch64-unknown-linux-musl" then {
|
||||||
rustVersion = "1.63.0";
|
rustVersion = "1.63.0";
|
||||||
extraRustComponents = [ "clippy" ];
|
extraRustComponents = [ "clippy" ];
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -15,7 +15,7 @@ type: application
|
||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
|
|
@ -18,6 +18,9 @@ metadata:
|
||||||
name: {{ $fullName }}-s3-api
|
name: {{ $fullName }}-s3-api
|
||||||
labels:
|
labels:
|
||||||
{{- include "garage.labels" . | nindent 4 }}
|
{{- include "garage.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.s3.api.labels }}
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
{{- with .Values.ingress.s3.api.annotations }}
|
{{- with .Values.ingress.s3.api.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
|
@ -80,6 +83,9 @@ metadata:
|
||||||
name: {{ $fullName }}-s3-web
|
name: {{ $fullName }}-s3-web
|
||||||
labels:
|
labels:
|
||||||
{{- include "garage.labels" . | nindent 4 }}
|
{{- include "garage.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.s3.web.labels }}
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
{{- with .Values.ingress.s3.web.annotations }}
|
{{- with .Values.ingress.s3.web.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
|
|
|
@ -85,14 +85,15 @@ service:
|
||||||
ingress:
|
ingress:
|
||||||
s3:
|
s3:
|
||||||
api:
|
api:
|
||||||
enabled: true
|
enabled: false
|
||||||
# Rely either on the className or the annotation below but not both
|
# Rely either on the className or the annotation below but not both
|
||||||
# replace "nginx" by an Ingress controller
|
# replace "nginx" by an Ingress controller
|
||||||
# you can find examples here https://kubernetes.io/docs/concepts/services-networking/ingress-controllers
|
# you can find examples here https://kubernetes.io/docs/concepts/services-networking/ingress-controllers
|
||||||
className: "nginx"
|
# className: "nginx"
|
||||||
annotations:
|
annotations: {}
|
||||||
# kubernetes.io/ingress.class: "nginx"
|
# kubernetes.io/ingress.class: "nginx"
|
||||||
# kubernetes.io/tls-acme: "true"
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
labels: {}
|
||||||
hosts:
|
hosts:
|
||||||
- host: "s3.garage.tld" # garage S3 API endpoint
|
- host: "s3.garage.tld" # garage S3 API endpoint
|
||||||
paths:
|
paths:
|
||||||
|
@ -107,11 +108,15 @@ ingress:
|
||||||
# hosts:
|
# hosts:
|
||||||
# - kubernetes.docker.internal
|
# - kubernetes.docker.internal
|
||||||
web:
|
web:
|
||||||
enabled: true
|
enabled: false
|
||||||
className: "nginx"
|
# Rely either on the className or the annotation below but not both
|
||||||
|
# replace "nginx" by an Ingress controller
|
||||||
|
# you can find examples here https://kubernetes.io/docs/concepts/services-networking/ingress-controllers
|
||||||
|
# className: "nginx"
|
||||||
annotations: {}
|
annotations: {}
|
||||||
# kubernetes.io/ingress.class: nginx
|
# kubernetes.io/ingress.class: nginx
|
||||||
# kubernetes.io/tls-acme: "true"
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
labels: {}
|
||||||
hosts:
|
hosts:
|
||||||
- host: "*.web.garage.tld" # wildcard website access with bucket name prefix
|
- host: "*.web.garage.tld" # wildcard website access with bucket name prefix
|
||||||
paths:
|
paths:
|
||||||
|
|
1053
script/telemetry/grafana-garage-dashboard-prometheus.json
Normal file
14
shell.nix
|
@ -71,13 +71,25 @@ function refresh_cache {
|
||||||
for attr in clippy.amd64 test.amd64 pkgs.{amd64,i386,arm,arm64}.{debug,release}; do
|
for attr in clippy.amd64 test.amd64 pkgs.{amd64,i386,arm,arm64}.{debug,release}; do
|
||||||
echo "Updating cache for ''${attr}"
|
echo "Updating cache for ''${attr}"
|
||||||
derivation=$(nix-instantiate --attr ''${attr})
|
derivation=$(nix-instantiate --attr ''${attr})
|
||||||
nix copy \
|
nix copy -j8 \
|
||||||
--to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/tmp/nix-signing-key.sec' \
|
--to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/tmp/nix-signing-key.sec' \
|
||||||
$(nix-store -qR ''${derivation%\!bin})
|
$(nix-store -qR ''${derivation%\!bin})
|
||||||
done
|
done
|
||||||
rm /tmp/nix-signing-key.sec
|
rm /tmp/nix-signing-key.sec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refresh_flake_cache {
|
||||||
|
pass show deuxfleurs/nix_priv_key > /tmp/nix-signing-key.sec
|
||||||
|
for attr in packages.x86_64-linux.default; do
|
||||||
|
echo "Updating cache for ''${attr}"
|
||||||
|
derivation=$(nix path-info --derivation ".#''${attr}")
|
||||||
|
nix copy -j8 \
|
||||||
|
--to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/tmp/nix-signing-key.sec' \
|
||||||
|
$(nix-store -qR ''${derivation})
|
||||||
|
done
|
||||||
|
rm /tmp/nix-signing-key.sec
|
||||||
|
}
|
||||||
|
|
||||||
function to_s3 {
|
function to_s3 {
|
||||||
aws \
|
aws \
|
||||||
--endpoint-url https://garage.deuxfleurs.fr \
|
--endpoint-url https://garage.deuxfleurs.fr \
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "garage_api"
|
name = "garage_api"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
@ -14,11 +14,11 @@ path = "lib.rs"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
garage_model = { version = "0.8.0", path = "../model" }
|
garage_model = { version = "0.8.1", path = "../model" }
|
||||||
garage_table = { version = "0.8.0", path = "../table" }
|
garage_table = { version = "0.8.1", path = "../table" }
|
||||||
garage_block = { version = "0.8.0", path = "../block" }
|
garage_block = { version = "0.8.1", path = "../block" }
|
||||||
garage_util = { version = "0.8.0", path = "../util" }
|
garage_util = { version = "0.8.1", path = "../util" }
|
||||||
garage_rpc = { version = "0.8.0", path = "../rpc" }
|
garage_rpc = { version = "0.8.1", path = "../rpc" }
|
||||||
|
|
||||||
async-trait = "0.1.7"
|
async-trait = "0.1.7"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
|
|
|
@ -15,6 +15,7 @@ use opentelemetry_prometheus::PrometheusExporter;
|
||||||
use prometheus::{Encoder, TextEncoder};
|
use prometheus::{Encoder, TextEncoder};
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
|
use garage_rpc::system::ClusterHealthStatus;
|
||||||
use garage_util::error::Error as GarageError;
|
use garage_util::error::Error as GarageError;
|
||||||
|
|
||||||
use crate::generic_server::*;
|
use crate::generic_server::*;
|
||||||
|
@ -76,6 +77,31 @@ impl AdminApiServer {
|
||||||
.body(Body::empty())?)
|
.body(Body::empty())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_health(&self) -> Result<Response<Body>, Error> {
|
||||||
|
let health = self.garage.system.health();
|
||||||
|
|
||||||
|
let (status, status_str) = match health.status {
|
||||||
|
ClusterHealthStatus::Healthy => (StatusCode::OK, "Garage is fully operational"),
|
||||||
|
ClusterHealthStatus::Degraded => (
|
||||||
|
StatusCode::OK,
|
||||||
|
"Garage is operational but some storage nodes are unavailable",
|
||||||
|
),
|
||||||
|
ClusterHealthStatus::Unavailable => (
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
"Quorum is not available for some/all partitions, reads and writes will fail",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let status_str = format!(
|
||||||
|
"{}\nConsult the full health check API endpoint at /v0/health for more details\n",
|
||||||
|
status_str
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(status)
|
||||||
|
.header(http::header::CONTENT_TYPE, "text/plain")
|
||||||
|
.body(Body::from(status_str))?)
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_metrics(&self) -> Result<Response<Body>, Error> {
|
fn handle_metrics(&self) -> Result<Response<Body>, Error> {
|
||||||
#[cfg(feature = "metrics")]
|
#[cfg(feature = "metrics")]
|
||||||
{
|
{
|
||||||
|
@ -124,6 +150,7 @@ impl ApiHandler for AdminApiServer {
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<Body>, Error> {
|
||||||
let expected_auth_header =
|
let expected_auth_header =
|
||||||
match endpoint.authorization_type() {
|
match endpoint.authorization_type() {
|
||||||
|
Authorization::None => None,
|
||||||
Authorization::MetricsToken => self.metrics_token.as_ref(),
|
Authorization::MetricsToken => self.metrics_token.as_ref(),
|
||||||
Authorization::AdminToken => match &self.admin_token {
|
Authorization::AdminToken => match &self.admin_token {
|
||||||
None => return Err(Error::forbidden(
|
None => return Err(Error::forbidden(
|
||||||
|
@ -147,8 +174,10 @@ impl ApiHandler for AdminApiServer {
|
||||||
|
|
||||||
match endpoint {
|
match endpoint {
|
||||||
Endpoint::Options => self.handle_options(&req),
|
Endpoint::Options => self.handle_options(&req),
|
||||||
|
Endpoint::Health => self.handle_health(),
|
||||||
Endpoint::Metrics => self.handle_metrics(),
|
Endpoint::Metrics => self.handle_metrics(),
|
||||||
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,
|
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,
|
||||||
|
Endpoint::GetClusterHealth => handle_get_cluster_health(&self.garage).await,
|
||||||
Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await,
|
Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await,
|
||||||
// Layout
|
// Layout
|
||||||
Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await,
|
Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await,
|
||||||
|
|
|
@ -210,7 +210,7 @@ async fn bucket_info_results(
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
objects: counters.get(OBJECTS).cloned().unwrap_or_default(),
|
objects: counters.get(OBJECTS).cloned().unwrap_or_default(),
|
||||||
bytes: counters.get(BYTES).cloned().unwrap_or_default(),
|
bytes: counters.get(BYTES).cloned().unwrap_or_default(),
|
||||||
unfinshed_uploads: counters
|
unfinished_uploads: counters
|
||||||
.get(UNFINISHED_UPLOADS)
|
.get(UNFINISHED_UPLOADS)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
@ -234,7 +234,7 @@ struct GetBucketInfoResult {
|
||||||
keys: Vec<GetBucketInfoKey>,
|
keys: Vec<GetBucketInfoKey>,
|
||||||
objects: i64,
|
objects: i64,
|
||||||
bytes: i64,
|
bytes: i64,
|
||||||
unfinshed_uploads: i64,
|
unfinished_uploads: i64,
|
||||||
quotas: ApiBucketQuotas,
|
quotas: ApiBucketQuotas,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,11 @@ pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<
|
||||||
Ok(json_ok_response(&res)?)
|
Ok(json_ok_response(&res)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_get_cluster_health(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
|
||||||
|
let health = garage.system.health();
|
||||||
|
Ok(json_ok_response(&health)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_connect_cluster_nodes(
|
pub async fn handle_connect_cluster_nodes(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
|
|
|
@ -6,6 +6,7 @@ use crate::admin::error::*;
|
||||||
use crate::router_macros::*;
|
use crate::router_macros::*;
|
||||||
|
|
||||||
pub enum Authorization {
|
pub enum Authorization {
|
||||||
|
None,
|
||||||
MetricsToken,
|
MetricsToken,
|
||||||
AdminToken,
|
AdminToken,
|
||||||
}
|
}
|
||||||
|
@ -16,8 +17,10 @@ router_match! {@func
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Endpoint {
|
pub enum Endpoint {
|
||||||
Options,
|
Options,
|
||||||
|
Health,
|
||||||
Metrics,
|
Metrics,
|
||||||
GetClusterStatus,
|
GetClusterStatus,
|
||||||
|
GetClusterHealth,
|
||||||
ConnectClusterNodes,
|
ConnectClusterNodes,
|
||||||
// Layout
|
// Layout
|
||||||
GetClusterLayout,
|
GetClusterLayout,
|
||||||
|
@ -88,8 +91,10 @@ impl Endpoint {
|
||||||
|
|
||||||
let res = router_match!(@gen_path_parser (req.method(), path, query) [
|
let res = router_match!(@gen_path_parser (req.method(), path, query) [
|
||||||
OPTIONS _ => Options,
|
OPTIONS _ => Options,
|
||||||
|
GET "/health" => Health,
|
||||||
GET "/metrics" => Metrics,
|
GET "/metrics" => Metrics,
|
||||||
GET "/v0/status" => GetClusterStatus,
|
GET "/v0/status" => GetClusterStatus,
|
||||||
|
GET "/v0/health" => GetClusterHealth,
|
||||||
POST "/v0/connect" => ConnectClusterNodes,
|
POST "/v0/connect" => ConnectClusterNodes,
|
||||||
// Layout endpoints
|
// Layout endpoints
|
||||||
GET "/v0/layout" => GetClusterLayout,
|
GET "/v0/layout" => GetClusterLayout,
|
||||||
|
@ -130,6 +135,7 @@ impl Endpoint {
|
||||||
/// Get the kind of authorization which is required to perform the operation.
|
/// Get the kind of authorization which is required to perform the operation.
|
||||||
pub fn authorization_type(&self) -> Authorization {
|
pub fn authorization_type(&self) -> Authorization {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Health => Authorization::None,
|
||||||
Self::Metrics => Authorization::MetricsToken,
|
Self::Metrics => Authorization::MetricsToken,
|
||||||
_ => Authorization::AdminToken,
|
_ => Authorization::AdminToken,
|
||||||
}
|
}
|
||||||
|
@ -137,9 +143,13 @@ impl Endpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
generateQueryParameters! {
|
generateQueryParameters! {
|
||||||
|
keywords: [],
|
||||||
|
fields: [
|
||||||
|
"format" => format,
|
||||||
"id" => id,
|
"id" => id,
|
||||||
"search" => search,
|
"search" => search,
|
||||||
"globalAlias" => global_alias,
|
"globalAlias" => global_alias,
|
||||||
"alias" => alias,
|
"alias" => alias,
|
||||||
"accessKeyId" => access_key_id
|
"accessKeyId" => access_key_id
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ impl Endpoint {
|
||||||
fn from_get(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
fn from_get(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None),
|
(query.keyword.take().unwrap_or_default(), partition_key, query, None),
|
||||||
key: [
|
key: [
|
||||||
EMPTY if causality_token => PollItem (query::sort_key, query::causality_token, opt_parse::timeout),
|
EMPTY if causality_token => PollItem (query::sort_key, query::causality_token, opt_parse::timeout),
|
||||||
EMPTY => ReadItem (query::sort_key),
|
EMPTY => ReadItem (query::sort_key),
|
||||||
|
@ -111,7 +111,7 @@ impl Endpoint {
|
||||||
fn from_search(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
fn from_search(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None),
|
(query.keyword.take().unwrap_or_default(), partition_key, query, None),
|
||||||
key: [
|
key: [
|
||||||
],
|
],
|
||||||
no_key: [
|
no_key: [
|
||||||
|
@ -125,7 +125,7 @@ impl Endpoint {
|
||||||
fn from_head(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
fn from_head(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None),
|
(query.keyword.take().unwrap_or_default(), partition_key, query, None),
|
||||||
key: [
|
key: [
|
||||||
EMPTY => HeadObject(opt_parse::part_number, query_opt::version_id),
|
EMPTY => HeadObject(opt_parse::part_number, query_opt::version_id),
|
||||||
],
|
],
|
||||||
|
@ -140,7 +140,7 @@ impl Endpoint {
|
||||||
fn from_post(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
fn from_post(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None),
|
(query.keyword.take().unwrap_or_default(), partition_key, query, None),
|
||||||
key: [
|
key: [
|
||||||
],
|
],
|
||||||
no_key: [
|
no_key: [
|
||||||
|
@ -155,7 +155,7 @@ impl Endpoint {
|
||||||
fn from_put(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
fn from_put(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None),
|
(query.keyword.take().unwrap_or_default(), partition_key, query, None),
|
||||||
key: [
|
key: [
|
||||||
EMPTY => InsertItem (query::sort_key),
|
EMPTY => InsertItem (query::sort_key),
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ impl Endpoint {
|
||||||
fn from_delete(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
fn from_delete(partition_key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None),
|
(query.keyword.take().unwrap_or_default(), partition_key, query, None),
|
||||||
key: [
|
key: [
|
||||||
EMPTY => DeleteItem (query::sort_key),
|
EMPTY => DeleteItem (query::sort_key),
|
||||||
],
|
],
|
||||||
|
@ -232,6 +232,11 @@ impl Endpoint {
|
||||||
|
|
||||||
// parameter name => struct field
|
// parameter name => struct field
|
||||||
generateQueryParameters! {
|
generateQueryParameters! {
|
||||||
|
keywords: [
|
||||||
|
"delete" => DELETE,
|
||||||
|
"search" => SEARCH
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
"prefix" => prefix,
|
"prefix" => prefix,
|
||||||
"start" => start,
|
"start" => start,
|
||||||
"causality_token" => causality_token,
|
"causality_token" => causality_token,
|
||||||
|
@ -240,13 +245,5 @@ generateQueryParameters! {
|
||||||
"reverse" => reverse,
|
"reverse" => reverse,
|
||||||
"sort_key" => sort_key,
|
"sort_key" => sort_key,
|
||||||
"timeout" => timeout
|
"timeout" => timeout
|
||||||
}
|
]
|
||||||
|
|
||||||
mod keywords {
|
|
||||||
//! This module contain all query parameters with no associated value
|
|
||||||
//! used to differentiate endpoints.
|
|
||||||
pub const EMPTY: &str = "";
|
|
||||||
|
|
||||||
pub const DELETE: &str = "delete";
|
|
||||||
pub const SEARCH: &str = "search";
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,9 @@ macro_rules! router_match {
|
||||||
(@match $enum:expr , [ $($endpoint:ident,)* ]) => {{
|
(@match $enum:expr , [ $($endpoint:ident,)* ]) => {{
|
||||||
// usage: router_match {@match my_enum, [ VariantWithField1, VariantWithField2 ..] }
|
// usage: router_match {@match my_enum, [ VariantWithField1, VariantWithField2 ..] }
|
||||||
// returns true if the variant was one of the listed variants, false otherwise.
|
// returns true if the variant was one of the listed variants, false otherwise.
|
||||||
use Endpoint::*;
|
|
||||||
match $enum {
|
match $enum {
|
||||||
$(
|
$(
|
||||||
$endpoint { .. } => true,
|
Endpoint::$endpoint { .. } => true,
|
||||||
)*
|
)*
|
||||||
_ => false
|
_ => false
|
||||||
}
|
}
|
||||||
|
@ -15,10 +14,9 @@ macro_rules! router_match {
|
||||||
(@extract $enum:expr , $param:ident, [ $($endpoint:ident,)* ]) => {{
|
(@extract $enum:expr , $param:ident, [ $($endpoint:ident,)* ]) => {{
|
||||||
// usage: router_match {@extract my_enum, field_name, [ VariantWithField1, VariantWithField2 ..] }
|
// usage: router_match {@extract my_enum, field_name, [ VariantWithField1, VariantWithField2 ..] }
|
||||||
// returns Some(field_value), or None if the variant was not one of the listed variants.
|
// returns Some(field_value), or None if the variant was not one of the listed variants.
|
||||||
use Endpoint::*;
|
|
||||||
match $enum {
|
match $enum {
|
||||||
$(
|
$(
|
||||||
$endpoint {$param, ..} => Some($param),
|
Endpoint::$endpoint {$param, ..} => Some($param),
|
||||||
)*
|
)*
|
||||||
_ => None
|
_ => None
|
||||||
}
|
}
|
||||||
|
@ -28,10 +26,9 @@ macro_rules! router_match {
|
||||||
$($meth:ident $path:pat $(if $required:ident)? => $api:ident $(($($conv:ident :: $param:ident),*))?,)*
|
$($meth:ident $path:pat $(if $required:ident)? => $api:ident $(($($conv:ident :: $param:ident),*))?,)*
|
||||||
]) => {{
|
]) => {{
|
||||||
{
|
{
|
||||||
use Endpoint::*;
|
|
||||||
match ($method, $reqpath) {
|
match ($method, $reqpath) {
|
||||||
$(
|
$(
|
||||||
(&Method::$meth, $path) if true $(&& $query.$required.is_some())? => $api {
|
(&Method::$meth, $path) if true $(&& $query.$required.is_some())? => Endpoint::$api {
|
||||||
$($(
|
$($(
|
||||||
$param: router_match!(@@parse_param $query, $conv, $param),
|
$param: router_match!(@@parse_param $query, $conv, $param),
|
||||||
)*)?
|
)*)?
|
||||||
|
@ -60,11 +57,9 @@ macro_rules! router_match {
|
||||||
// ]
|
// ]
|
||||||
// }
|
// }
|
||||||
// See in from_{method} for more detailed usage.
|
// See in from_{method} for more detailed usage.
|
||||||
use Endpoint::*;
|
|
||||||
use keywords::*;
|
|
||||||
match ($keyword, !$key.is_empty()){
|
match ($keyword, !$key.is_empty()){
|
||||||
$(
|
$(
|
||||||
($kw_k, true) if true $(&& $query.$required_k.is_some())? $(&& $header.contains_key($header_k))? => Ok($api_k {
|
(Keyword::$kw_k, true) if true $(&& $query.$required_k.is_some())? $(&& $header.contains_key($header_k))? => Ok(Endpoint::$api_k {
|
||||||
$key,
|
$key,
|
||||||
$($(
|
$($(
|
||||||
$param_k: router_match!(@@parse_param $query, $conv_k, $param_k),
|
$param_k: router_match!(@@parse_param $query, $conv_k, $param_k),
|
||||||
|
@ -72,7 +67,7 @@ macro_rules! router_match {
|
||||||
}),
|
}),
|
||||||
)*
|
)*
|
||||||
$(
|
$(
|
||||||
($kw_nk, false) $(if $query.$required_nk.is_some())? $(if $header.contains($header_nk))? => Ok($api_nk {
|
(Keyword::$kw_nk, false) $(if $query.$required_nk.is_some())? $(if $header.contains($header_nk))? => Ok(Endpoint::$api_nk {
|
||||||
$($(
|
$($(
|
||||||
$param_nk: router_match!(@@parse_param $query, $conv_nk, $param_nk),
|
$param_nk: router_match!(@@parse_param $query, $conv_nk, $param_nk),
|
||||||
)*)?
|
)*)?
|
||||||
|
@ -144,14 +139,39 @@ macro_rules! router_match {
|
||||||
/// This macro is used to generate part of the code in this module. It must be called only one, and
|
/// This macro is used to generate part of the code in this module. It must be called only one, and
|
||||||
/// is useless outside of this module.
|
/// is useless outside of this module.
|
||||||
macro_rules! generateQueryParameters {
|
macro_rules! generateQueryParameters {
|
||||||
( $($rest:expr => $name:ident),* ) => {
|
(
|
||||||
|
keywords: [ $($kw_param:expr => $kw_name: ident),* ],
|
||||||
|
fields: [ $($f_param:expr => $f_name:ident),* ]
|
||||||
|
) => {
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
enum Keyword {
|
||||||
|
EMPTY,
|
||||||
|
$( $kw_name, )*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Keyword {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Keyword::EMPTY => write!(f, "``"),
|
||||||
|
$( Keyword::$kw_name => write!(f, "`{}`", $kw_param), )*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Keyword {
|
||||||
|
fn default() -> Self {
|
||||||
|
Keyword::EMPTY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Struct containing all query parameters used in endpoints. Think of it as an HashMap,
|
/// Struct containing all query parameters used in endpoints. Think of it as an HashMap,
|
||||||
/// but with keys statically known.
|
/// but with keys statically known.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct QueryParameters<'a> {
|
struct QueryParameters<'a> {
|
||||||
keyword: Option<Cow<'a, str>>,
|
keyword: Option<Keyword>,
|
||||||
$(
|
$(
|
||||||
$name: Option<Cow<'a, str>>,
|
$f_name: Option<Cow<'a, str>>,
|
||||||
)*
|
)*
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,34 +180,29 @@ macro_rules! generateQueryParameters {
|
||||||
fn from_query(query: &'a str) -> Result<Self, Error> {
|
fn from_query(query: &'a str) -> Result<Self, Error> {
|
||||||
let mut res: Self = Default::default();
|
let mut res: Self = Default::default();
|
||||||
for (k, v) in url::form_urlencoded::parse(query.as_bytes()) {
|
for (k, v) in url::form_urlencoded::parse(query.as_bytes()) {
|
||||||
let repeated = match k.as_ref() {
|
match k.as_ref() {
|
||||||
$(
|
$(
|
||||||
$rest => if !v.is_empty() {
|
$kw_param => if let Some(prev_kw) = res.keyword.replace(Keyword::$kw_name) {
|
||||||
res.$name.replace(v).is_some()
|
return Err(Error::bad_request(format!(
|
||||||
} else {
|
"Multiple keywords: '{}' and '{}'", prev_kw, $kw_param
|
||||||
false
|
)));
|
||||||
|
},
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
$f_param => if !v.is_empty() {
|
||||||
|
if res.$f_name.replace(v).is_some() {
|
||||||
|
return Err(Error::bad_request(format!(
|
||||||
|
"Query parameter repeated: '{}'", k
|
||||||
|
)));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)*
|
)*
|
||||||
_ => {
|
_ => {
|
||||||
if k.starts_with("response-") || k.starts_with("X-Amz-") {
|
if !(k.starts_with("response-") || k.starts_with("X-Amz-")) {
|
||||||
false
|
|
||||||
} else if v.as_ref().is_empty() {
|
|
||||||
if res.keyword.replace(k).is_some() {
|
|
||||||
return Err(Error::bad_request("Multiple keywords"));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
debug!("Received an unknown query parameter: '{}'", k);
|
debug!("Received an unknown query parameter: '{}'", k);
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if repeated {
|
|
||||||
return Err(Error::bad_request(format!(
|
|
||||||
"Query parameter repeated: '{}'",
|
|
||||||
k
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
@ -198,8 +213,8 @@ macro_rules! generateQueryParameters {
|
||||||
if self.keyword.is_some() {
|
if self.keyword.is_some() {
|
||||||
Some("Keyword not used")
|
Some("Keyword not used")
|
||||||
} $(
|
} $(
|
||||||
else if self.$name.is_some() {
|
else if self.$f_name.is_some() {
|
||||||
Some(concat!("'", $rest, "'"))
|
Some(concat!("'", $f_param, "'"))
|
||||||
}
|
}
|
||||||
)* else {
|
)* else {
|
||||||
None
|
None
|
||||||
|
|
|
@ -161,6 +161,15 @@ pub async fn handle_create_bucket(
|
||||||
return Err(CommonError::BucketAlreadyExists.into());
|
return Err(CommonError::BucketAlreadyExists.into());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Check user is allowed to create bucket
|
||||||
|
if !key_params.allow_create_bucket.get() {
|
||||||
|
return Err(CommonError::Forbidden(format!(
|
||||||
|
"Access key {} is not allowed to create buckets",
|
||||||
|
api_key.key_id
|
||||||
|
))
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
// Create the bucket!
|
// Create the bucket!
|
||||||
if !is_valid_bucket_name(&bucket_name) {
|
if !is_valid_bucket_name(&bucket_name) {
|
||||||
return Err(Error::bad_request(format!(
|
return Err(Error::bad_request(format!(
|
||||||
|
|
|
@ -355,7 +355,7 @@ impl Endpoint {
|
||||||
fn from_get(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
fn from_get(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), key, query, None),
|
(query.keyword.take().unwrap_or_default(), key, query, None),
|
||||||
key: [
|
key: [
|
||||||
EMPTY if upload_id => ListParts (query::upload_id, opt_parse::max_parts, opt_parse::part_number_marker),
|
EMPTY if upload_id => ListParts (query::upload_id, opt_parse::max_parts, opt_parse::part_number_marker),
|
||||||
EMPTY => GetObject (query_opt::version_id, opt_parse::part_number),
|
EMPTY => GetObject (query_opt::version_id, opt_parse::part_number),
|
||||||
|
@ -412,7 +412,7 @@ impl Endpoint {
|
||||||
fn from_head(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
fn from_head(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), key, query, None),
|
(query.keyword.take().unwrap_or_default(), key, query, None),
|
||||||
key: [
|
key: [
|
||||||
EMPTY => HeadObject(opt_parse::part_number, query_opt::version_id),
|
EMPTY => HeadObject(opt_parse::part_number, query_opt::version_id),
|
||||||
],
|
],
|
||||||
|
@ -426,7 +426,7 @@ impl Endpoint {
|
||||||
fn from_post(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
fn from_post(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), key, query, None),
|
(query.keyword.take().unwrap_or_default(), key, query, None),
|
||||||
key: [
|
key: [
|
||||||
EMPTY if upload_id => CompleteMultipartUpload (query::upload_id),
|
EMPTY if upload_id => CompleteMultipartUpload (query::upload_id),
|
||||||
RESTORE => RestoreObject (query_opt::version_id),
|
RESTORE => RestoreObject (query_opt::version_id),
|
||||||
|
@ -448,7 +448,7 @@ impl Endpoint {
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), key, query, headers),
|
(query.keyword.take().unwrap_or_default(), key, query, headers),
|
||||||
key: [
|
key: [
|
||||||
EMPTY if part_number header "x-amz-copy-source" => UploadPartCopy (parse::part_number, query::upload_id),
|
EMPTY if part_number header "x-amz-copy-source" => UploadPartCopy (parse::part_number, query::upload_id),
|
||||||
EMPTY header "x-amz-copy-source" => CopyObject,
|
EMPTY header "x-amz-copy-source" => CopyObject,
|
||||||
|
@ -490,7 +490,7 @@ impl Endpoint {
|
||||||
fn from_delete(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
fn from_delete(key: String, query: &mut QueryParameters<'_>) -> Result<Self, Error> {
|
||||||
router_match! {
|
router_match! {
|
||||||
@gen_parser
|
@gen_parser
|
||||||
(query.keyword.take().unwrap_or_default().as_ref(), key, query, None),
|
(query.keyword.take().unwrap_or_default(), key, query, None),
|
||||||
key: [
|
key: [
|
||||||
EMPTY if upload_id => AbortMultipartUpload (query::upload_id),
|
EMPTY if upload_id => AbortMultipartUpload (query::upload_id),
|
||||||
EMPTY => DeleteObject (query_opt::version_id),
|
EMPTY => DeleteObject (query_opt::version_id),
|
||||||
|
@ -624,6 +624,39 @@ impl Endpoint {
|
||||||
|
|
||||||
// parameter name => struct field
|
// parameter name => struct field
|
||||||
generateQueryParameters! {
|
generateQueryParameters! {
|
||||||
|
keywords: [
|
||||||
|
"accelerate" => ACCELERATE,
|
||||||
|
"acl" => ACL,
|
||||||
|
"analytics" => ANALYTICS,
|
||||||
|
"cors" => CORS,
|
||||||
|
"delete" => DELETE,
|
||||||
|
"encryption" => ENCRYPTION,
|
||||||
|
"intelligent-tiering" => INTELLIGENT_TIERING,
|
||||||
|
"inventory" => INVENTORY,
|
||||||
|
"legal-hold" => LEGAL_HOLD,
|
||||||
|
"lifecycle" => LIFECYCLE,
|
||||||
|
"location" => LOCATION,
|
||||||
|
"logging" => LOGGING,
|
||||||
|
"metrics" => METRICS,
|
||||||
|
"notification" => NOTIFICATION,
|
||||||
|
"object-lock" => OBJECT_LOCK,
|
||||||
|
"ownershipControls" => OWNERSHIP_CONTROLS,
|
||||||
|
"policy" => POLICY,
|
||||||
|
"policyStatus" => POLICY_STATUS,
|
||||||
|
"publicAccessBlock" => PUBLIC_ACCESS_BLOCK,
|
||||||
|
"replication" => REPLICATION,
|
||||||
|
"requestPayment" => REQUEST_PAYMENT,
|
||||||
|
"restore" => RESTORE,
|
||||||
|
"retention" => RETENTION,
|
||||||
|
"select" => SELECT,
|
||||||
|
"tagging" => TAGGING,
|
||||||
|
"torrent" => TORRENT,
|
||||||
|
"uploads" => UPLOADS,
|
||||||
|
"versioning" => VERSIONING,
|
||||||
|
"versions" => VERSIONS,
|
||||||
|
"website" => WEBSITE
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
"continuation-token" => continuation_token,
|
"continuation-token" => continuation_token,
|
||||||
"delimiter" => delimiter,
|
"delimiter" => delimiter,
|
||||||
"encoding-type" => encoding_type,
|
"encoding-type" => encoding_type,
|
||||||
|
@ -644,43 +677,7 @@ generateQueryParameters! {
|
||||||
"upload-id-marker" => upload_id_marker,
|
"upload-id-marker" => upload_id_marker,
|
||||||
"versionId" => version_id,
|
"versionId" => version_id,
|
||||||
"version-id-marker" => version_id_marker
|
"version-id-marker" => version_id_marker
|
||||||
}
|
]
|
||||||
|
|
||||||
mod keywords {
|
|
||||||
//! This module contain all query parameters with no associated value S3 uses to differentiate
|
|
||||||
//! endpoints.
|
|
||||||
pub const EMPTY: &str = "";
|
|
||||||
|
|
||||||
pub const ACCELERATE: &str = "accelerate";
|
|
||||||
pub const ACL: &str = "acl";
|
|
||||||
pub const ANALYTICS: &str = "analytics";
|
|
||||||
pub const CORS: &str = "cors";
|
|
||||||
pub const DELETE: &str = "delete";
|
|
||||||
pub const ENCRYPTION: &str = "encryption";
|
|
||||||
pub const INTELLIGENT_TIERING: &str = "intelligent-tiering";
|
|
||||||
pub const INVENTORY: &str = "inventory";
|
|
||||||
pub const LEGAL_HOLD: &str = "legal-hold";
|
|
||||||
pub const LIFECYCLE: &str = "lifecycle";
|
|
||||||
pub const LOCATION: &str = "location";
|
|
||||||
pub const LOGGING: &str = "logging";
|
|
||||||
pub const METRICS: &str = "metrics";
|
|
||||||
pub const NOTIFICATION: &str = "notification";
|
|
||||||
pub const OBJECT_LOCK: &str = "object-lock";
|
|
||||||
pub const OWNERSHIP_CONTROLS: &str = "ownershipControls";
|
|
||||||
pub const POLICY: &str = "policy";
|
|
||||||
pub const POLICY_STATUS: &str = "policyStatus";
|
|
||||||
pub const PUBLIC_ACCESS_BLOCK: &str = "publicAccessBlock";
|
|
||||||
pub const REPLICATION: &str = "replication";
|
|
||||||
pub const REQUEST_PAYMENT: &str = "requestPayment";
|
|
||||||
pub const RESTORE: &str = "restore";
|
|
||||||
pub const RETENTION: &str = "retention";
|
|
||||||
pub const SELECT: &str = "select";
|
|
||||||
pub const TAGGING: &str = "tagging";
|
|
||||||
pub const TORRENT: &str = "torrent";
|
|
||||||
pub const UPLOADS: &str = "uploads";
|
|
||||||
pub const VERSIONING: &str = "versioning";
|
|
||||||
pub const VERSIONS: &str = "versions";
|
|
||||||
pub const WEBSITE: &str = "website";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "garage_block"
|
name = "garage_block"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
@ -14,10 +14,10 @@ path = "lib.rs"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
garage_db = { version = "0.8.0", path = "../db" }
|
garage_db = { version = "0.8.1", path = "../db" }
|
||||||
garage_rpc = { version = "0.8.0", path = "../rpc" }
|
garage_rpc = { version = "0.8.1", path = "../rpc" }
|
||||||
garage_util = { version = "0.8.0", path = "../util" }
|
garage_util = { version = "0.8.1", path = "../util" }
|
||||||
garage_table = { version = "0.8.0", path = "../table" }
|
garage_table = { version = "0.8.1", path = "../table" }
|
||||||
|
|
||||||
opentelemetry = "0.17"
|
opentelemetry = "0.17"
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,15 @@ pub struct BlockManager {
|
||||||
tx_scrub_command: mpsc::Sender<ScrubWorkerCommand>,
|
tx_scrub_command: mpsc::Sender<ScrubWorkerCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct BlockResyncErrorInfo {
|
||||||
|
pub hash: Hash,
|
||||||
|
pub refcount: u64,
|
||||||
|
pub error_count: u64,
|
||||||
|
pub last_try: u64,
|
||||||
|
pub next_try: u64,
|
||||||
|
}
|
||||||
|
|
||||||
// This custom struct contains functions that must only be ran
|
// This custom struct contains functions that must only be ran
|
||||||
// when the lock is held. We ensure that it is the case by storing
|
// when the lock is held. We ensure that it is the case by storing
|
||||||
// it INSIDE a Mutex.
|
// it INSIDE a Mutex.
|
||||||
|
@ -114,7 +123,8 @@ impl BlockManager {
|
||||||
.netapp
|
.netapp
|
||||||
.endpoint("garage_block/manager.rs/Rpc".to_string());
|
.endpoint("garage_block/manager.rs/Rpc".to_string());
|
||||||
|
|
||||||
let metrics = BlockManagerMetrics::new(resync.queue.clone(), resync.errors.clone());
|
let metrics =
|
||||||
|
BlockManagerMetrics::new(rc.rc.clone(), resync.queue.clone(), resync.errors.clone());
|
||||||
|
|
||||||
let (scrub_tx, scrub_rx) = mpsc::channel(1);
|
let (scrub_tx, scrub_rx) = mpsc::channel(1);
|
||||||
|
|
||||||
|
@ -309,11 +319,41 @@ impl BlockManager {
|
||||||
Ok(self.rc.rc.len()?)
|
Ok(self.rc.rc.len()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get number of items in the refcount table
|
||||||
|
pub fn rc_fast_len(&self) -> Result<Option<usize>, Error> {
|
||||||
|
Ok(self.rc.rc.fast_len()?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Send command to start/stop/manager scrub worker
|
/// Send command to start/stop/manager scrub worker
|
||||||
pub async fn send_scrub_command(&self, cmd: ScrubWorkerCommand) {
|
pub async fn send_scrub_command(&self, cmd: ScrubWorkerCommand) {
|
||||||
let _ = self.tx_scrub_command.send(cmd).await;
|
let _ = self.tx_scrub_command.send(cmd).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the reference count of a block
|
||||||
|
pub fn get_block_rc(&self, hash: &Hash) -> Result<u64, Error> {
|
||||||
|
Ok(self.rc.get_block_rc(hash)?.as_u64())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all resync errors
|
||||||
|
pub fn list_resync_errors(&self) -> Result<Vec<BlockResyncErrorInfo>, Error> {
|
||||||
|
let mut blocks = Vec::with_capacity(self.resync.errors.len());
|
||||||
|
for ent in self.resync.errors.iter()? {
|
||||||
|
let (hash, cnt) = ent?;
|
||||||
|
let cnt = ErrorCounter::decode(&cnt);
|
||||||
|
blocks.push(BlockResyncErrorInfo {
|
||||||
|
hash: Hash::try_from(&hash).unwrap(),
|
||||||
|
refcount: 0,
|
||||||
|
error_count: cnt.errors,
|
||||||
|
last_try: cnt.last_try,
|
||||||
|
next_try: cnt.next_try(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for block in blocks.iter_mut() {
|
||||||
|
block.refcount = self.get_block_rc(&block.hash)?;
|
||||||
|
}
|
||||||
|
Ok(blocks)
|
||||||
|
}
|
||||||
|
|
||||||
//// ----- Managing the reference counter ----
|
//// ----- Managing the reference counter ----
|
||||||
|
|
||||||
/// Increment the number of time a block is used, putting it to resynchronization if it is
|
/// Increment the number of time a block is used, putting it to resynchronization if it is
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use opentelemetry::{global, metrics::*};
|
use opentelemetry::{global, metrics::*};
|
||||||
|
|
||||||
|
use garage_db as db;
|
||||||
use garage_db::counted_tree_hack::CountedTree;
|
use garage_db::counted_tree_hack::CountedTree;
|
||||||
|
|
||||||
/// TableMetrics reference all counter used for metrics
|
/// TableMetrics reference all counter used for metrics
|
||||||
pub struct BlockManagerMetrics {
|
pub struct BlockManagerMetrics {
|
||||||
|
pub(crate) _rc_size: ValueObserver<u64>,
|
||||||
pub(crate) _resync_queue_len: ValueObserver<u64>,
|
pub(crate) _resync_queue_len: ValueObserver<u64>,
|
||||||
pub(crate) _resync_errored_blocks: ValueObserver<u64>,
|
pub(crate) _resync_errored_blocks: ValueObserver<u64>,
|
||||||
|
|
||||||
|
@ -23,9 +25,17 @@ pub struct BlockManagerMetrics {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockManagerMetrics {
|
impl BlockManagerMetrics {
|
||||||
pub fn new(resync_queue: CountedTree, resync_errors: CountedTree) -> Self {
|
pub fn new(rc_tree: db::Tree, resync_queue: CountedTree, resync_errors: CountedTree) -> Self {
|
||||||
let meter = global::meter("garage_model/block");
|
let meter = global::meter("garage_model/block");
|
||||||
Self {
|
Self {
|
||||||
|
_rc_size: meter
|
||||||
|
.u64_value_observer("block.rc_size", move |observer| {
|
||||||
|
if let Ok(Some(v)) = rc_tree.fast_len() {
|
||||||
|
observer.observe(v as u64, &[])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_description("Number of blocks known to the reference counter")
|
||||||
|
.init(),
|
||||||
_resync_queue_len: meter
|
_resync_queue_len: meter
|
||||||
.u64_value_observer("block.resync_queue_length", move |observer| {
|
.u64_value_observer("block.resync_queue_length", move |observer| {
|
||||||
observer.observe(resync_queue.len() as u64, &[])
|
observer.observe(resync_queue.len() as u64, &[])
|
||||||
|
|
|
@ -169,4 +169,11 @@ impl RcEntry {
|
||||||
pub(crate) fn is_needed(&self) -> bool {
|
pub(crate) fn is_needed(&self) -> bool {
|
||||||
!self.is_deletable()
|
!self.is_deletable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_u64(&self) -> u64 {
|
||||||
|
match self {
|
||||||
|
RcEntry::Present { count } => *count,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ impl Worker for RepairWorker {
|
||||||
"Block repair worker".into()
|
"Block repair worker".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info(&self) -> Option<String> {
|
fn status(&self) -> WorkerStatus {
|
||||||
match self.block_iter.as_ref() {
|
match self.block_iter.as_ref() {
|
||||||
None => {
|
None => {
|
||||||
let idx_bytes = self
|
let idx_bytes = self
|
||||||
|
@ -66,9 +66,20 @@ impl Worker for RepairWorker {
|
||||||
} else {
|
} else {
|
||||||
idx_bytes
|
idx_bytes
|
||||||
};
|
};
|
||||||
Some(format!("Phase 1: {}", hex::encode(idx_bytes)))
|
WorkerStatus {
|
||||||
|
progress: Some("0.00%".into()),
|
||||||
|
freeform: vec![format!(
|
||||||
|
"Currently in phase 1, iterator position: {}",
|
||||||
|
hex::encode(idx_bytes)
|
||||||
|
)],
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
Some(bi) => Some(format!("Phase 2: {:.2}% done", bi.progress() * 100.)),
|
}
|
||||||
|
Some(bi) => WorkerStatus {
|
||||||
|
progress: Some(format!("{:.2}%", bi.progress() * 100.)),
|
||||||
|
freeform: vec!["Currently in phase 2".into()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,29 +282,28 @@ impl Worker for ScrubWorker {
|
||||||
"Block scrub worker".into()
|
"Block scrub worker".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info(&self) -> Option<String> {
|
fn status(&self) -> WorkerStatus {
|
||||||
let s = match &self.work {
|
let mut s = WorkerStatus {
|
||||||
ScrubWorkerState::Running(bsi) => format!(
|
persistent_errors: Some(self.persisted.corruptions_detected),
|
||||||
"{:.2}% done (tranquility = {})",
|
tranquility: Some(self.persisted.tranquility),
|
||||||
bsi.progress() * 100.,
|
..Default::default()
|
||||||
self.persisted.tranquility
|
|
||||||
),
|
|
||||||
ScrubWorkerState::Paused(bsi, rt) => {
|
|
||||||
format!(
|
|
||||||
"Paused, {:.2}% done, resumes at {}",
|
|
||||||
bsi.progress() * 100.,
|
|
||||||
msec_to_rfc3339(*rt)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ScrubWorkerState::Finished => format!(
|
|
||||||
"Last completed scrub: {}",
|
|
||||||
msec_to_rfc3339(self.persisted.time_last_complete_scrub)
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
Some(format!(
|
match &self.work {
|
||||||
"{} ; corruptions detected: {}",
|
ScrubWorkerState::Running(bsi) => {
|
||||||
s, self.persisted.corruptions_detected
|
s.progress = Some(format!("{:.2}%", bsi.progress() * 100.));
|
||||||
))
|
}
|
||||||
|
ScrubWorkerState::Paused(bsi, rt) => {
|
||||||
|
s.progress = Some(format!("{:.2}%", bsi.progress() * 100.));
|
||||||
|
s.freeform = vec![format!("Scrub paused, resumes at {}", msec_to_rfc3339(*rt))];
|
||||||
|
}
|
||||||
|
ScrubWorkerState::Finished => {
|
||||||
|
s.freeform = vec![format!(
|
||||||
|
"Last scrub completed at {}",
|
||||||
|
msec_to_rfc3339(self.persisted.time_last_complete_scrub)
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn work(&mut self, _must_exit: &mut watch::Receiver<bool>) -> Result<WorkerState, Error> {
|
async fn work(&mut self, _must_exit: &mut watch::Receiver<bool>) -> Result<WorkerState, Error> {
|
||||||
|
|
|
@ -123,6 +123,24 @@ impl BlockResyncManager {
|
||||||
Ok(self.errors.len())
|
Ok(self.errors.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear the error counter for a block and put it in queue immediately
|
||||||
|
pub fn clear_backoff(&self, hash: &Hash) -> Result<(), Error> {
|
||||||
|
let now = now_msec();
|
||||||
|
if let Some(ec) = self.errors.get(hash)? {
|
||||||
|
let mut ec = ErrorCounter::decode(&ec);
|
||||||
|
if ec.errors > 0 {
|
||||||
|
ec.last_try = now - ec.delay_msec();
|
||||||
|
self.errors.insert(hash, ec.encode())?;
|
||||||
|
self.put_to_resync_at(hash, now)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::Message(format!(
|
||||||
|
"Block {:?} was not in an errored state",
|
||||||
|
hash
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Resync loop ----
|
// ---- Resync loop ----
|
||||||
|
|
||||||
// This part manages a queue of blocks that need to be
|
// This part manages a queue of blocks that need to be
|
||||||
|
@ -257,7 +275,7 @@ impl BlockResyncManager {
|
||||||
|
|
||||||
if let Err(e) = &res {
|
if let Err(e) = &res {
|
||||||
manager.metrics.resync_error_counter.add(1);
|
manager.metrics.resync_error_counter.add(1);
|
||||||
warn!("Error when resyncing {:?}: {}", hash, e);
|
error!("Error when resyncing {:?}: {}", hash, e);
|
||||||
|
|
||||||
let err_counter = match self.errors.get(hash.as_slice())? {
|
let err_counter = match self.errors.get(hash.as_slice())? {
|
||||||
Some(ec) => ErrorCounter::decode(&ec).add1(now + 1),
|
Some(ec) => ErrorCounter::decode(&ec).add1(now + 1),
|
||||||
|
@ -477,27 +495,22 @@ impl Worker for ResyncWorker {
|
||||||
format!("Block resync worker #{}", self.index + 1)
|
format!("Block resync worker #{}", self.index + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info(&self) -> Option<String> {
|
fn status(&self) -> WorkerStatus {
|
||||||
let persisted = self.manager.resync.persisted.load();
|
let persisted = self.manager.resync.persisted.load();
|
||||||
|
|
||||||
if self.index >= persisted.n_workers {
|
if self.index >= persisted.n_workers {
|
||||||
return Some("(unused)".into());
|
return WorkerStatus {
|
||||||
|
freeform: vec!["This worker is currently disabled".into()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ret = vec![];
|
WorkerStatus {
|
||||||
ret.push(format!("tranquility = {}", persisted.tranquility));
|
queue_length: Some(self.manager.resync.queue_len().unwrap_or(0) as u64),
|
||||||
|
tranquility: Some(persisted.tranquility),
|
||||||
let qlen = self.manager.resync.queue_len().unwrap_or(0);
|
persistent_errors: Some(self.manager.resync.errors_len().unwrap_or(0) as u64),
|
||||||
if qlen > 0 {
|
..Default::default()
|
||||||
ret.push(format!("{} blocks in queue", qlen));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let elen = self.manager.resync.errors_len().unwrap_or(0);
|
|
||||||
if elen > 0 {
|
|
||||||
ret.push(format!("{} blocks in error state", elen));
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(ret.join(", "))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn work(&mut self, _must_exit: &mut watch::Receiver<bool>) -> Result<WorkerState, Error> {
|
async fn work(&mut self, _must_exit: &mut watch::Receiver<bool>) -> Result<WorkerState, Error> {
|
||||||
|
@ -545,9 +558,9 @@ impl Worker for ResyncWorker {
|
||||||
/// and the time of the last try.
|
/// and the time of the last try.
|
||||||
/// Used to implement exponential backoff.
|
/// Used to implement exponential backoff.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
struct ErrorCounter {
|
pub(crate) struct ErrorCounter {
|
||||||
errors: u64,
|
pub(crate) errors: u64,
|
||||||
last_try: u64,
|
pub(crate) last_try: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorCounter {
|
impl ErrorCounter {
|
||||||
|
@ -558,12 +571,13 @@ impl ErrorCounter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode(data: &[u8]) -> Self {
|
pub(crate) fn decode(data: &[u8]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
errors: u64::from_be_bytes(data[0..8].try_into().unwrap()),
|
errors: u64::from_be_bytes(data[0..8].try_into().unwrap()),
|
||||||
last_try: u64::from_be_bytes(data[8..16].try_into().unwrap()),
|
last_try: u64::from_be_bytes(data[8..16].try_into().unwrap()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode(&self) -> Vec<u8> {
|
fn encode(&self) -> Vec<u8> {
|
||||||
[
|
[
|
||||||
u64::to_be_bytes(self.errors),
|
u64::to_be_bytes(self.errors),
|
||||||
|
@ -583,7 +597,8 @@ impl ErrorCounter {
|
||||||
(RESYNC_RETRY_DELAY.as_millis() as u64)
|
(RESYNC_RETRY_DELAY.as_millis() as u64)
|
||||||
<< std::cmp::min(self.errors - 1, RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER)
|
<< std::cmp::min(self.errors - 1, RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER)
|
||||||
}
|
}
|
||||||
fn next_try(&self) -> u64 {
|
|
||||||
|
pub(crate) fn next_try(&self) -> u64 {
|
||||||
self.last_try + self.delay_msec()
|
self.last_try + self.delay_msec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "garage_db"
|
name = "garage_db"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
@ -33,6 +33,7 @@ pretty_env_logger = { version = "0.4", optional = true }
|
||||||
mktemp = "0.4"
|
mktemp = "0.4"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = [ "sled" ]
|
||||||
bundled-libs = [ "rusqlite/bundled" ]
|
bundled-libs = [ "rusqlite/bundled" ]
|
||||||
cli = ["clap", "pretty_env_logger"]
|
cli = ["clap", "pretty_env_logger"]
|
||||||
lmdb = [ "heed" ]
|
lmdb = [ "heed" ]
|
||||||
|
|
|
@ -181,6 +181,10 @@ impl Tree {
|
||||||
pub fn len(&self) -> Result<usize> {
|
pub fn len(&self) -> Result<usize> {
|
||||||
self.0.len(self.1)
|
self.0.len(self.1)
|
||||||
}
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn fast_len(&self) -> Result<Option<usize>> {
|
||||||
|
self.0.fast_len(self.1)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn first(&self) -> Result<Option<(Value, Value)>> {
|
pub fn first(&self) -> Result<Option<(Value, Value)>> {
|
||||||
|
@ -323,6 +327,9 @@ pub(crate) trait IDb: Send + Sync {
|
||||||
|
|
||||||
fn get(&self, tree: usize, key: &[u8]) -> Result<Option<Value>>;
|
fn get(&self, tree: usize, key: &[u8]) -> Result<Option<Value>>;
|
||||||
fn len(&self, tree: usize) -> Result<usize>;
|
fn len(&self, tree: usize) -> Result<usize>;
|
||||||
|
fn fast_len(&self, _tree: usize) -> Result<Option<usize>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result<Option<Value>>;
|
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result<Option<Value>>;
|
||||||
fn remove(&self, tree: usize, key: &[u8]) -> Result<Option<Value>>;
|
fn remove(&self, tree: usize, key: &[u8]) -> Result<Option<Value>>;
|
||||||
|
|
|
@ -121,6 +121,10 @@ impl IDb for LmdbDb {
|
||||||
Ok(tree.len(&tx)?.try_into().unwrap())
|
Ok(tree.len(&tx)?.try_into().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fast_len(&self, tree: usize) -> Result<Option<usize>> {
|
||||||
|
Ok(Some(self.len(tree)?))
|
||||||
|
}
|
||||||
|
|
||||||
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result<Option<Value>> {
|
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result<Option<Value>> {
|
||||||
let tree = self.get_tree(tree)?;
|
let tree = self.get_tree(tree)?;
|
||||||
let mut tx = self.db.write_txn()?;
|
let mut tx = self.db.write_txn()?;
|
||||||
|
|
|
@ -144,6 +144,10 @@ impl IDb for SqliteDb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fast_len(&self, tree: usize) -> Result<Option<usize>> {
|
||||||
|
Ok(Some(self.len(tree)?))
|
||||||
|
}
|
||||||
|
|
||||||
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result<Option<Value>> {
|
fn insert(&self, tree: usize, key: &[u8], value: &[u8]) -> Result<Option<Value>> {
|
||||||
trace!("insert {}: lock db", tree);
|
trace!("insert {}: lock db", tree);
|
||||||
let this = self.0.lock().unwrap();
|
let this = self.0.lock().unwrap();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "garage"
|
name = "garage"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
@ -21,14 +21,14 @@ path = "tests/lib.rs"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
garage_db = { version = "0.8.0", path = "../db" }
|
garage_db = { version = "0.8.1", path = "../db" }
|
||||||
garage_api = { version = "0.8.0", path = "../api" }
|
garage_api = { version = "0.8.1", path = "../api" }
|
||||||
garage_block = { version = "0.8.0", path = "../block" }
|
garage_block = { version = "0.8.1", path = "../block" }
|
||||||
garage_model = { version = "0.8.0", path = "../model" }
|
garage_model = { version = "0.8.1", path = "../model" }
|
||||||
garage_rpc = { version = "0.8.0", path = "../rpc" }
|
garage_rpc = { version = "0.8.1", path = "../rpc" }
|
||||||
garage_table = { version = "0.8.0", path = "../table" }
|
garage_table = { version = "0.8.1", path = "../table" }
|
||||||
garage_util = { version = "0.8.0", path = "../util" }
|
garage_util = { version = "0.8.1", path = "../util" }
|
||||||
garage_web = { version = "0.8.0", path = "../web" }
|
garage_web = { version = "0.8.1", path = "../web" }
|
||||||
|
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
|
@ -36,7 +36,7 @@ bytesize = "1.1"
|
||||||
timeago = "0.3"
|
timeago = "0.3"
|
||||||
parse_duration = "2.1"
|
parse_duration = "2.1"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
tracing = { version = "0.1.30", features = ["log-always"] }
|
tracing = { version = "0.1.30" }
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
async-trait = "0.1.7"
|
async-trait = "0.1.7"
|
||||||
|
|
|
@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use garage_util::crdt::*;
|
use garage_util::crdt::*;
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
use garage_util::error::Error as GarageError;
|
use garage_util::error::Error as GarageError;
|
||||||
|
use garage_util::formater::format_table_to_string;
|
||||||
use garage_util::time::*;
|
use garage_util::time::*;
|
||||||
|
|
||||||
use garage_table::replication::*;
|
use garage_table::replication::*;
|
||||||
|
@ -15,6 +16,7 @@ use garage_table::*;
|
||||||
|
|
||||||
use garage_rpc::*;
|
use garage_rpc::*;
|
||||||
|
|
||||||
|
use garage_block::manager::BlockResyncErrorInfo;
|
||||||
use garage_block::repair::ScrubWorkerCommand;
|
use garage_block::repair::ScrubWorkerCommand;
|
||||||
|
|
||||||
use garage_model::bucket_alias_table::*;
|
use garage_model::bucket_alias_table::*;
|
||||||
|
@ -24,6 +26,8 @@ use garage_model::helper::error::{Error, OkOrBadRequest};
|
||||||
use garage_model::key_table::*;
|
use garage_model::key_table::*;
|
||||||
use garage_model::migrate::Migrate;
|
use garage_model::migrate::Migrate;
|
||||||
use garage_model::permission::*;
|
use garage_model::permission::*;
|
||||||
|
use garage_model::s3::object_table::*;
|
||||||
|
use garage_model::s3::version_table::Version;
|
||||||
|
|
||||||
use crate::cli::*;
|
use crate::cli::*;
|
||||||
use crate::repair::online::launch_online_repair;
|
use crate::repair::online::launch_online_repair;
|
||||||
|
@ -38,7 +42,8 @@ pub enum AdminRpc {
|
||||||
LaunchRepair(RepairOpt),
|
LaunchRepair(RepairOpt),
|
||||||
Migrate(MigrateOpt),
|
Migrate(MigrateOpt),
|
||||||
Stats(StatsOpt),
|
Stats(StatsOpt),
|
||||||
Worker(WorkerOpt),
|
Worker(WorkerOperation),
|
||||||
|
BlockOperation(BlockOperation),
|
||||||
|
|
||||||
// Replies
|
// Replies
|
||||||
Ok(String),
|
Ok(String),
|
||||||
|
@ -54,6 +59,13 @@ pub enum AdminRpc {
|
||||||
HashMap<usize, garage_util::background::WorkerInfo>,
|
HashMap<usize, garage_util::background::WorkerInfo>,
|
||||||
WorkerListOpt,
|
WorkerListOpt,
|
||||||
),
|
),
|
||||||
|
WorkerInfo(usize, garage_util::background::WorkerInfo),
|
||||||
|
BlockErrorList(Vec<BlockResyncErrorInfo>),
|
||||||
|
BlockInfo {
|
||||||
|
hash: Hash,
|
||||||
|
refcount: u64,
|
||||||
|
versions: Vec<Result<Version, Uuid>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rpc for AdminRpc {
|
impl Rpc for AdminRpc {
|
||||||
|
@ -73,6 +85,8 @@ impl AdminRpcHandler {
|
||||||
admin
|
admin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================ BUCKET COMMANDS ====================
|
||||||
|
|
||||||
async fn handle_bucket_cmd(&self, cmd: &BucketOperation) -> Result<AdminRpc, Error> {
|
async fn handle_bucket_cmd(&self, cmd: &BucketOperation) -> Result<AdminRpc, Error> {
|
||||||
match cmd {
|
match cmd {
|
||||||
BucketOperation::List => self.handle_list_buckets().await,
|
BucketOperation::List => self.handle_list_buckets().await,
|
||||||
|
@ -551,6 +565,8 @@ impl AdminRpcHandler {
|
||||||
Ok(AdminRpc::Ok(ret))
|
Ok(AdminRpc::Ok(ret))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================ KEY COMMANDS ====================
|
||||||
|
|
||||||
async fn handle_key_cmd(&self, cmd: &KeyOperation) -> Result<AdminRpc, Error> {
|
async fn handle_key_cmd(&self, cmd: &KeyOperation) -> Result<AdminRpc, Error> {
|
||||||
match cmd {
|
match cmd {
|
||||||
KeyOperation::List => self.handle_list_keys().await,
|
KeyOperation::List => self.handle_list_keys().await,
|
||||||
|
@ -688,6 +704,8 @@ impl AdminRpcHandler {
|
||||||
Ok(AdminRpc::KeyInfo(key, relevant_buckets))
|
Ok(AdminRpc::KeyInfo(key, relevant_buckets))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================ MIGRATION COMMANDS ====================
|
||||||
|
|
||||||
async fn handle_migrate(self: &Arc<Self>, opt: MigrateOpt) -> Result<AdminRpc, Error> {
|
async fn handle_migrate(self: &Arc<Self>, opt: MigrateOpt) -> Result<AdminRpc, Error> {
|
||||||
if !opt.yes {
|
if !opt.yes {
|
||||||
return Err(Error::BadRequest(
|
return Err(Error::BadRequest(
|
||||||
|
@ -704,6 +722,8 @@ impl AdminRpcHandler {
|
||||||
Ok(AdminRpc::Ok("Migration successfull.".into()))
|
Ok(AdminRpc::Ok("Migration successfull.".into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================ REPAIR COMMANDS ====================
|
||||||
|
|
||||||
async fn handle_launch_repair(self: &Arc<Self>, opt: RepairOpt) -> Result<AdminRpc, Error> {
|
async fn handle_launch_repair(self: &Arc<Self>, opt: RepairOpt) -> Result<AdminRpc, Error> {
|
||||||
if !opt.yes {
|
if !opt.yes {
|
||||||
return Err(Error::BadRequest(
|
return Err(Error::BadRequest(
|
||||||
|
@ -747,6 +767,8 @@ impl AdminRpcHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================ STATS COMMANDS ====================
|
||||||
|
|
||||||
async fn handle_stats(&self, opt: StatsOpt) -> Result<AdminRpc, Error> {
|
async fn handle_stats(&self, opt: StatsOpt) -> Result<AdminRpc, Error> {
|
||||||
if opt.all_nodes {
|
if opt.all_nodes {
|
||||||
let mut ret = String::new();
|
let mut ret = String::new();
|
||||||
|
@ -763,11 +785,12 @@ impl AdminRpcHandler {
|
||||||
match self
|
match self
|
||||||
.endpoint
|
.endpoint
|
||||||
.call(&node_id, AdminRpc::Stats(opt), PRIO_NORMAL)
|
.call(&node_id, AdminRpc::Stats(opt), PRIO_NORMAL)
|
||||||
.await?
|
.await
|
||||||
{
|
{
|
||||||
Ok(AdminRpc::Ok(s)) => writeln!(&mut ret, "{}", s).unwrap(),
|
Ok(Ok(AdminRpc::Ok(s))) => writeln!(&mut ret, "{}", s).unwrap(),
|
||||||
Ok(x) => writeln!(&mut ret, "Bad answer: {:?}", x).unwrap(),
|
Ok(Ok(x)) => writeln!(&mut ret, "Bad answer: {:?}", x).unwrap(),
|
||||||
Err(e) => writeln!(&mut ret, "Error: {}", e).unwrap(),
|
Ok(Err(e)) => writeln!(&mut ret, "Remote error: {}", e).unwrap(),
|
||||||
|
Err(e) => writeln!(&mut ret, "Network error: {}", e).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(AdminRpc::Ok(ret))
|
Ok(AdminRpc::Ok(ret))
|
||||||
|
@ -787,6 +810,7 @@ impl AdminRpcHandler {
|
||||||
.unwrap_or_else(|| "(unknown)".into()),
|
.unwrap_or_else(|| "(unknown)".into()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
writeln!(&mut ret, "\nDatabase engine: {}", self.garage.db.engine()).unwrap();
|
writeln!(&mut ret, "\nDatabase engine: {}", self.garage.db.engine()).unwrap();
|
||||||
|
|
||||||
// Gather ring statistics
|
// Gather ring statistics
|
||||||
|
@ -805,21 +829,38 @@ impl AdminRpcHandler {
|
||||||
writeln!(&mut ret, " {:?} {}", n, c).unwrap();
|
writeln!(&mut ret, " {:?} {}", n, c).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.gather_table_stats(&mut ret, &self.garage.bucket_table, &opt)?;
|
// Gather table statistics
|
||||||
self.gather_table_stats(&mut ret, &self.garage.key_table, &opt)?;
|
let mut table = vec![" Table\tItems\tMklItems\tMklTodo\tGcTodo".into()];
|
||||||
self.gather_table_stats(&mut ret, &self.garage.object_table, &opt)?;
|
table.push(self.gather_table_stats(&self.garage.bucket_table, opt.detailed)?);
|
||||||
self.gather_table_stats(&mut ret, &self.garage.version_table, &opt)?;
|
table.push(self.gather_table_stats(&self.garage.key_table, opt.detailed)?);
|
||||||
self.gather_table_stats(&mut ret, &self.garage.block_ref_table, &opt)?;
|
table.push(self.gather_table_stats(&self.garage.object_table, opt.detailed)?);
|
||||||
|
table.push(self.gather_table_stats(&self.garage.version_table, opt.detailed)?);
|
||||||
|
table.push(self.gather_table_stats(&self.garage.block_ref_table, opt.detailed)?);
|
||||||
|
write!(
|
||||||
|
&mut ret,
|
||||||
|
"\nTable stats:\n{}",
|
||||||
|
format_table_to_string(table)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Gather block manager statistics
|
||||||
writeln!(&mut ret, "\nBlock manager stats:").unwrap();
|
writeln!(&mut ret, "\nBlock manager stats:").unwrap();
|
||||||
if opt.detailed {
|
let rc_len = if opt.detailed {
|
||||||
|
self.garage.block_manager.rc_len()?.to_string()
|
||||||
|
} else {
|
||||||
|
self.garage
|
||||||
|
.block_manager
|
||||||
|
.rc_fast_len()?
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.unwrap_or_else(|| "NC".into())
|
||||||
|
};
|
||||||
|
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut ret,
|
&mut ret,
|
||||||
" number of RC entries (~= number of blocks): {}",
|
" number of RC entries (~= number of blocks): {}",
|
||||||
self.garage.block_manager.rc_len()?
|
rc_len
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut ret,
|
&mut ret,
|
||||||
" resync queue length: {}",
|
" resync queue length: {}",
|
||||||
|
@ -833,67 +874,84 @@ impl AdminRpcHandler {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
if !opt.detailed {
|
||||||
|
writeln!(&mut ret, "\nIf values are missing (marked as NC), consider adding the --detailed flag - this will be slow.").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gather_table_stats<F, R>(
|
fn gather_table_stats<F, R>(
|
||||||
&self,
|
&self,
|
||||||
to: &mut String,
|
|
||||||
t: &Arc<Table<F, R>>,
|
t: &Arc<Table<F, R>>,
|
||||||
opt: &StatsOpt,
|
detailed: bool,
|
||||||
) -> Result<(), Error>
|
) -> Result<String, Error>
|
||||||
where
|
where
|
||||||
F: TableSchema + 'static,
|
F: TableSchema + 'static,
|
||||||
R: TableReplication + 'static,
|
R: TableReplication + 'static,
|
||||||
{
|
{
|
||||||
writeln!(to, "\nTable stats for {}", F::TABLE_NAME).unwrap();
|
let (data_len, mkl_len) = if detailed {
|
||||||
if opt.detailed {
|
(
|
||||||
writeln!(
|
t.data.store.len().map_err(GarageError::from)?.to_string(),
|
||||||
to,
|
t.merkle_updater.merkle_tree_len()?.to_string(),
|
||||||
" number of items: {}",
|
|
||||||
t.data.store.len().map_err(GarageError::from)?
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
} else {
|
||||||
writeln!(
|
(
|
||||||
to,
|
t.data
|
||||||
" Merkle tree size: {}",
|
.store
|
||||||
t.merkle_updater.merkle_tree_len()?
|
.fast_len()
|
||||||
|
.map_err(GarageError::from)?
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.unwrap_or_else(|| "NC".into()),
|
||||||
|
t.merkle_updater
|
||||||
|
.merkle_tree_fast_len()?
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.unwrap_or_else(|| "NC".into()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
};
|
||||||
}
|
|
||||||
writeln!(
|
|
||||||
to,
|
|
||||||
" Merkle updater todo queue length: {}",
|
|
||||||
t.merkle_updater.todo_len()?
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
writeln!(to, " GC todo queue length: {}", t.data.gc_todo_len()?).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(format!(
|
||||||
|
" {}\t{}\t{}\t{}\t{}",
|
||||||
|
F::TABLE_NAME,
|
||||||
|
data_len,
|
||||||
|
mkl_len,
|
||||||
|
t.merkle_updater.todo_len()?,
|
||||||
|
t.data.gc_todo_len()?
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----
|
// ================ WORKER COMMANDS ====================
|
||||||
|
|
||||||
async fn handle_worker_cmd(&self, opt: WorkerOpt) -> Result<AdminRpc, Error> {
|
async fn handle_worker_cmd(&self, cmd: &WorkerOperation) -> Result<AdminRpc, Error> {
|
||||||
match opt.cmd {
|
match cmd {
|
||||||
WorkerCmd::List { opt } => {
|
WorkerOperation::List { opt } => {
|
||||||
let workers = self.garage.background.get_worker_info();
|
let workers = self.garage.background.get_worker_info();
|
||||||
Ok(AdminRpc::WorkerList(workers, opt))
|
Ok(AdminRpc::WorkerList(workers, *opt))
|
||||||
}
|
}
|
||||||
WorkerCmd::Set { opt } => match opt {
|
WorkerOperation::Info { tid } => {
|
||||||
|
let info = self
|
||||||
|
.garage
|
||||||
|
.background
|
||||||
|
.get_worker_info()
|
||||||
|
.get(tid)
|
||||||
|
.ok_or_bad_request(format!("No worker with TID {}", tid))?
|
||||||
|
.clone();
|
||||||
|
Ok(AdminRpc::WorkerInfo(*tid, info))
|
||||||
|
}
|
||||||
|
WorkerOperation::Set { opt } => match opt {
|
||||||
WorkerSetCmd::ScrubTranquility { tranquility } => {
|
WorkerSetCmd::ScrubTranquility { tranquility } => {
|
||||||
let scrub_command = ScrubWorkerCommand::SetTranquility(tranquility);
|
let scrub_command = ScrubWorkerCommand::SetTranquility(*tranquility);
|
||||||
self.garage
|
self.garage
|
||||||
.block_manager
|
.block_manager
|
||||||
.send_scrub_command(scrub_command)
|
.send_scrub_command(scrub_command)
|
||||||
.await;
|
.await;
|
||||||
Ok(AdminRpc::Ok("Scrub tranquility updated".into()))
|
Ok(AdminRpc::Ok("Scrub tranquility updated".into()))
|
||||||
}
|
}
|
||||||
WorkerSetCmd::ResyncNWorkers { n_workers } => {
|
WorkerSetCmd::ResyncWorkerCount { worker_count } => {
|
||||||
self.garage
|
self.garage
|
||||||
.block_manager
|
.block_manager
|
||||||
.resync
|
.resync
|
||||||
.set_n_workers(n_workers)
|
.set_n_workers(*worker_count)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(AdminRpc::Ok("Number of resync workers updated".into()))
|
Ok(AdminRpc::Ok("Number of resync workers updated".into()))
|
||||||
}
|
}
|
||||||
|
@ -901,13 +959,154 @@ impl AdminRpcHandler {
|
||||||
self.garage
|
self.garage
|
||||||
.block_manager
|
.block_manager
|
||||||
.resync
|
.resync
|
||||||
.set_tranquility(tranquility)
|
.set_tranquility(*tranquility)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(AdminRpc::Ok("Resync tranquility updated".into()))
|
Ok(AdminRpc::Ok("Resync tranquility updated".into()))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================ BLOCK COMMANDS ====================
|
||||||
|
|
||||||
|
async fn handle_block_cmd(&self, cmd: &BlockOperation) -> Result<AdminRpc, Error> {
|
||||||
|
match cmd {
|
||||||
|
BlockOperation::ListErrors => Ok(AdminRpc::BlockErrorList(
|
||||||
|
self.garage.block_manager.list_resync_errors()?,
|
||||||
|
)),
|
||||||
|
BlockOperation::Info { hash } => {
|
||||||
|
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
|
||||||
|
let hash = Hash::try_from(&hash).ok_or_bad_request("invalid hash")?;
|
||||||
|
let refcount = self.garage.block_manager.get_block_rc(&hash)?;
|
||||||
|
let block_refs = self
|
||||||
|
.garage
|
||||||
|
.block_ref_table
|
||||||
|
.get_range(&hash, None, None, 10000, Default::default())
|
||||||
|
.await?;
|
||||||
|
let mut versions = vec![];
|
||||||
|
for br in block_refs {
|
||||||
|
if let Some(v) = self
|
||||||
|
.garage
|
||||||
|
.version_table
|
||||||
|
.get(&br.version, &EmptyKey)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
versions.push(Ok(v));
|
||||||
|
} else {
|
||||||
|
versions.push(Err(br.version));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(AdminRpc::BlockInfo {
|
||||||
|
hash,
|
||||||
|
refcount,
|
||||||
|
versions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
BlockOperation::RetryNow { all, blocks } => {
|
||||||
|
if *all {
|
||||||
|
if !blocks.is_empty() {
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
"--all was specified, cannot also specify blocks".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let blocks = self.garage.block_manager.list_resync_errors()?;
|
||||||
|
for b in blocks.iter() {
|
||||||
|
self.garage.block_manager.resync.clear_backoff(&b.hash)?;
|
||||||
|
}
|
||||||
|
Ok(AdminRpc::Ok(format!(
|
||||||
|
"{} blocks returned in queue for a retry now (check logs to see results)",
|
||||||
|
blocks.len()
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
for hash in blocks {
|
||||||
|
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
|
||||||
|
let hash = Hash::try_from(&hash).ok_or_bad_request("invalid hash")?;
|
||||||
|
self.garage.block_manager.resync.clear_backoff(&hash)?;
|
||||||
|
}
|
||||||
|
Ok(AdminRpc::Ok(format!(
|
||||||
|
"{} blocks returned in queue for a retry now (check logs to see results)",
|
||||||
|
blocks.len()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BlockOperation::Purge { yes, blocks } => {
|
||||||
|
if !yes {
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
"Pass the --yes flag to confirm block purge operation.".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut obj_dels = 0;
|
||||||
|
let mut ver_dels = 0;
|
||||||
|
|
||||||
|
for hash in blocks {
|
||||||
|
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
|
||||||
|
let hash = Hash::try_from(&hash).ok_or_bad_request("invalid hash")?;
|
||||||
|
let block_refs = self
|
||||||
|
.garage
|
||||||
|
.block_ref_table
|
||||||
|
.get_range(&hash, None, None, 10000, Default::default())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for br in block_refs {
|
||||||
|
let version = match self
|
||||||
|
.garage
|
||||||
|
.version_table
|
||||||
|
.get(&br.version, &EmptyKey)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some(v) => v,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(object) = self
|
||||||
|
.garage
|
||||||
|
.object_table
|
||||||
|
.get(&version.bucket_id, &version.key)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
let ov = object.versions().iter().rev().find(|v| v.is_complete());
|
||||||
|
if let Some(ov) = ov {
|
||||||
|
if ov.uuid == br.version {
|
||||||
|
let del_uuid = gen_uuid();
|
||||||
|
let deleted_object = Object::new(
|
||||||
|
version.bucket_id,
|
||||||
|
version.key.clone(),
|
||||||
|
vec![ObjectVersion {
|
||||||
|
uuid: del_uuid,
|
||||||
|
timestamp: ov.timestamp + 1,
|
||||||
|
state: ObjectVersionState::Complete(
|
||||||
|
ObjectVersionData::DeleteMarker,
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
self.garage.object_table.insert(&deleted_object).await?;
|
||||||
|
obj_dels += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !version.deleted.get() {
|
||||||
|
let deleted_version = Version::new(
|
||||||
|
version.uuid,
|
||||||
|
version.bucket_id,
|
||||||
|
version.key.clone(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
self.garage.version_table.insert(&deleted_version).await?;
|
||||||
|
ver_dels += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(AdminRpc::Ok(format!(
|
||||||
|
"{} blocks were purged: {} object deletion markers added, {} versions marked deleted",
|
||||||
|
blocks.len(),
|
||||||
|
obj_dels,
|
||||||
|
ver_dels
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -923,7 +1122,8 @@ impl EndpointHandler<AdminRpc> for AdminRpcHandler {
|
||||||
AdminRpc::Migrate(opt) => self.handle_migrate(opt.clone()).await,
|
AdminRpc::Migrate(opt) => self.handle_migrate(opt.clone()).await,
|
||||||
AdminRpc::LaunchRepair(opt) => self.handle_launch_repair(opt.clone()).await,
|
AdminRpc::LaunchRepair(opt) => self.handle_launch_repair(opt.clone()).await,
|
||||||
AdminRpc::Stats(opt) => self.handle_stats(opt.clone()).await,
|
AdminRpc::Stats(opt) => self.handle_stats(opt.clone()).await,
|
||||||
AdminRpc::Worker(opt) => self.handle_worker_cmd(opt.clone()).await,
|
AdminRpc::Worker(wo) => self.handle_worker_cmd(wo).await,
|
||||||
|
AdminRpc::BlockOperation(bo) => self.handle_block_cmd(bo).await,
|
||||||
m => Err(GarageError::unexpected_rpc_message(m).into()),
|
m => Err(GarageError::unexpected_rpc_message(m).into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,9 @@ pub async fn cli_command_dispatch(
|
||||||
}
|
}
|
||||||
Command::Stats(so) => cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::Stats(so)).await,
|
Command::Stats(so) => cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::Stats(so)).await,
|
||||||
Command::Worker(wo) => cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::Worker(wo)).await,
|
Command::Worker(wo) => cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::Worker(wo)).await,
|
||||||
|
Command::Block(bo) => {
|
||||||
|
cmd_admin(admin_rpc_endpoint, rpc_host, AdminRpc::BlockOperation(bo)).await
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,7 +189,20 @@ pub async fn cmd_admin(
|
||||||
print_key_info(&key, &rb);
|
print_key_info(&key, &rb);
|
||||||
}
|
}
|
||||||
AdminRpc::WorkerList(wi, wlo) => {
|
AdminRpc::WorkerList(wi, wlo) => {
|
||||||
print_worker_info(wi, wlo);
|
print_worker_list(wi, wlo);
|
||||||
|
}
|
||||||
|
AdminRpc::WorkerInfo(tid, wi) => {
|
||||||
|
print_worker_info(tid, wi);
|
||||||
|
}
|
||||||
|
AdminRpc::BlockErrorList(el) => {
|
||||||
|
print_block_error_list(el);
|
||||||
|
}
|
||||||
|
AdminRpc::BlockInfo {
|
||||||
|
hash,
|
||||||
|
refcount,
|
||||||
|
versions,
|
||||||
|
} => {
|
||||||
|
print_block_info(hash, refcount, versions);
|
||||||
}
|
}
|
||||||
r => {
|
r => {
|
||||||
error!("Unexpected response: {:?}", r);
|
error!("Unexpected response: {:?}", r);
|
||||||
|
|
|
@ -49,7 +49,11 @@ pub enum Command {
|
||||||
|
|
||||||
/// Manage background workers
|
/// Manage background workers
|
||||||
#[structopt(name = "worker", version = garage_version())]
|
#[structopt(name = "worker", version = garage_version())]
|
||||||
Worker(WorkerOpt),
|
Worker(WorkerOperation),
|
||||||
|
|
||||||
|
/// Low-level debug operations on data blocks
|
||||||
|
#[structopt(name = "block", version = garage_version())]
|
||||||
|
Block(BlockOperation),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
|
@ -502,20 +506,17 @@ pub struct StatsOpt {
|
||||||
pub detailed: bool,
|
pub detailed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Clone)]
|
|
||||||
pub struct WorkerOpt {
|
|
||||||
#[structopt(subcommand)]
|
|
||||||
pub cmd: WorkerCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||||
pub enum WorkerCmd {
|
pub enum WorkerOperation {
|
||||||
/// List all workers on Garage node
|
/// List all workers on Garage node
|
||||||
#[structopt(name = "list", version = garage_version())]
|
#[structopt(name = "list", version = garage_version())]
|
||||||
List {
|
List {
|
||||||
#[structopt(flatten)]
|
#[structopt(flatten)]
|
||||||
opt: WorkerListOpt,
|
opt: WorkerListOpt,
|
||||||
},
|
},
|
||||||
|
/// Get detailed information about a worker
|
||||||
|
#[structopt(name = "info", version = garage_version())]
|
||||||
|
Info { tid: usize },
|
||||||
/// Set worker parameter
|
/// Set worker parameter
|
||||||
#[structopt(name = "set", version = garage_version())]
|
#[structopt(name = "set", version = garage_version())]
|
||||||
Set {
|
Set {
|
||||||
|
@ -540,9 +541,41 @@ pub enum WorkerSetCmd {
|
||||||
#[structopt(name = "scrub-tranquility", version = garage_version())]
|
#[structopt(name = "scrub-tranquility", version = garage_version())]
|
||||||
ScrubTranquility { tranquility: u32 },
|
ScrubTranquility { tranquility: u32 },
|
||||||
/// Set number of concurrent block resync workers
|
/// Set number of concurrent block resync workers
|
||||||
#[structopt(name = "resync-n-workers", version = garage_version())]
|
#[structopt(name = "resync-worker-count", version = garage_version())]
|
||||||
ResyncNWorkers { n_workers: usize },
|
ResyncWorkerCount { worker_count: usize },
|
||||||
/// Set tranquility of block resync operations
|
/// Set tranquility of block resync operations
|
||||||
#[structopt(name = "resync-tranquility", version = garage_version())]
|
#[structopt(name = "resync-tranquility", version = garage_version())]
|
||||||
ResyncTranquility { tranquility: u32 },
|
ResyncTranquility { tranquility: u32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||||
|
pub enum BlockOperation {
|
||||||
|
/// List all blocks that currently have a resync error
|
||||||
|
#[structopt(name = "list-errors", version = garage_version())]
|
||||||
|
ListErrors,
|
||||||
|
/// Get detailed information about a single block
|
||||||
|
#[structopt(name = "info", version = garage_version())]
|
||||||
|
Info {
|
||||||
|
/// Hash of the block for which to retrieve information
|
||||||
|
hash: String,
|
||||||
|
},
|
||||||
|
/// Retry now the resync of one or many blocks
|
||||||
|
#[structopt(name = "retry-now", version = garage_version())]
|
||||||
|
RetryNow {
|
||||||
|
/// Retry all blocks that have a resync error
|
||||||
|
#[structopt(long = "all")]
|
||||||
|
all: bool,
|
||||||
|
/// Hashes of the block to retry to resync now
|
||||||
|
blocks: Vec<String>,
|
||||||
|
},
|
||||||
|
/// Delete all objects referencing a missing block
|
||||||
|
#[structopt(name = "purge", version = garage_version())]
|
||||||
|
Purge {
|
||||||
|
/// Mandatory to confirm this operation
|
||||||
|
#[structopt(long = "yes")]
|
||||||
|
yes: bool,
|
||||||
|
/// Hashes of the block to purge
|
||||||
|
#[structopt(required = true)]
|
||||||
|
blocks: Vec<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|