Compare commits

..

32 commits

Author SHA1 Message Date
9d83364ad9
itertools .unique() doesn't require sorted items 2022-12-11 18:30:02 +01:00
ec12d6c8dd
Slightly simplify code at places 2022-11-08 16:15:45 +01:00
217abdca18
Fix HTTP return code 2022-11-08 15:38:53 +01:00
fc2729cd81
Fix integration test 2022-11-08 15:19:46 +01:00
d75b37b018
Return more info when layout's .check() fails, fix compilation, fix test 2022-11-08 14:58:39 +01:00
73a4ca8b15
Use bytes as capacity units 2022-11-07 21:12:11 +01:00
fd5bc142b5
Ensure .sort() is called before counting unique items 2022-11-07 20:29:25 +01:00
ea5afc2511
Style improvements 2022-11-07 20:11:30 +01:00
28d7a49f63
Merge branch 'main' into optimal-layout 2022-11-07 12:20:59 +01:00
3039bb5d43
rm .gitattributes 2022-10-13 12:40:42 +02:00
bcdd1e0c33 Added some comment 2022-10-11 18:29:21 +02:00
e5664c9822 Improved the statistics displayed in layout show
corrected a few bugs
2022-10-11 17:17:13 +02:00
4abab246f1 cargo fmt 2022-10-10 17:21:13 +02:00
fcf9ac674a Tests written in layout.rs
added staged_parameters to ClusterLayout
removed the serde(default) -> will need a migration function
2022-10-10 17:19:25 +02:00
911eb17bd9 corrected warnings of cargo clippy 2022-10-06 14:53:57 +02:00
9407df60cc Corrected two bugs:
- self.node_id_vec was not properly updated when the previous ring was empty
- ClusterLayout::merge was not considering changes in the layout parameters
2022-10-06 12:54:51 +02:00
a951b6c452 Added a CLI command to update the parameters for the layout computation (for now, only the zone redundancy) 2022-10-05 16:04:19 +02:00
ceac3713d6 modifications in several files to :
- have consistent error return types
- store the zone redundancy in a Lww
- print the error and message in the CLI (TODO: for the server Api, should msg be returned in the body response?)
2022-10-05 15:29:48 +02:00
829f815a89 Merge remote-tracking branch 'origin/main' into optimal-layout 2022-10-04 18:14:49 +02:00
99f96b9564 deleted zone_redundancy from System struct 2022-10-04 18:09:24 +02:00
bd842e1388 Correction of a few bugs in the tests, modification of ClusterLayout::check 2022-09-22 19:30:01 +02:00
7f3249a237 New version of the algorithm that calculate the layout.
It takes as paramters the replication factor and the zone redundancy, computes the
largest partition size reachable with these constraints, and among the possible
assignation with this partition size, it computes the one that moves the least number
of partitions compared to the previous assignation.
This computation uses graph algorithms defined in graph_algo.rs
2022-09-21 14:39:59 +02:00
c4adbeed51 Added the section with description proofs of the parametric assignment computation in the optimal layout report 2022-09-10 13:51:12 +02:00
d38fb6c250 ignore log files in commit 2022-09-08 12:43:33 +02:00
81083dd415 Added a first draft version of the algorithm and analysis for the non-strict mode. 2022-08-19 21:21:41 +02:00
7b2c065c82 Merge branch 'optimal-layout' of https://git.deuxfleurs.fr/Deuxfleurs/garage into optimal-layout 2022-07-19 13:30:49 +02:00
03e3a1bd15 Added the latex report on the optimal layout algorithm 2022-07-18 22:35:29 +02:00
617f28bfa4
Correct small formatting issue 2022-05-05 14:21:57 +02:00
948ff93cf1 Corrected the warnings and errors issued by cargo clippy 2022-05-01 16:05:39 +02:00
3ba2c5b424
updated cargo.lock 2022-05-01 10:11:43 +02:00
2aeaddd5e2
Apply cargo fmt 2022-05-01 09:57:05 +02:00
c1d1646c4d
Change the way new layout assignations are computed.
The function now computes an optimal assignation (with respect to partition size) that minimizes the distance to the former assignation, using flow algorithms.

This commit was written by Mendes Oulamara <mendes.oulamara@pm.me>
2022-05-01 09:54:19 +02:00
144 changed files with 15051 additions and 8106 deletions

20
Cargo.lock generated
View file

@ -1048,7 +1048,7 @@ dependencies = [
[[package]] [[package]]
name = "garage" name = "garage"
version = "0.8.1" version = "0.8.0"
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.1" version = "0.8.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64", "base64",
@ -1141,7 +1141,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_block" name = "garage_block"
version = "0.8.1" version = "0.8.0"
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.1" version = "0.8.0"
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.1" version = "0.8.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
@ -1210,17 +1210,19 @@ dependencies = [
[[package]] [[package]]
name = "garage_rpc" name = "garage_rpc"
version = "0.8.1" version = "0.8.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
"bytes", "bytes",
"bytesize",
"err-derive", "err-derive",
"futures", "futures",
"futures-util", "futures-util",
"garage_util", "garage_util",
"gethostname", "gethostname",
"hex", "hex",
"itertools 0.10.3",
"k8s-openapi", "k8s-openapi",
"kube", "kube",
"kuska-sodiumoxide", "kuska-sodiumoxide",
@ -1241,7 +1243,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_table" name = "garage_table"
version = "0.8.1" version = "0.8.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@ -1263,7 +1265,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_util" name = "garage_util"
version = "0.8.1" version = "0.8.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
@ -1294,7 +1296,7 @@ dependencies = [
[[package]] [[package]]
name = "garage_web" name = "garage_web"
version = "0.8.1" version = "0.8.0"
dependencies = [ dependencies = [
"err-derive", "err-derive",
"futures", "futures",

193
Cargo.nix
View file

@ -32,7 +32,7 @@ args@{
ignoreLockHash, ignoreLockHash,
}: }:
let let
nixifiedLockHash = "463114c4544bfa9b442a43afc6b39eb588f5720825c7a246ba9188c4bdb52944"; nixifiedLockHash = "a68c589851ec1990d29cdc20e8b922b27c1a6b402b682f7b0d9a9e6258f25828";
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.1"; garage_db = rustPackages.unknown.garage_db."0.8.0";
garage_util = rustPackages.unknown.garage_util."0.8.1"; garage_util = rustPackages.unknown.garage_util."0.8.0";
garage_rpc = rustPackages.unknown.garage_rpc."0.8.1"; garage_rpc = rustPackages.unknown.garage_rpc."0.8.0";
garage_table = rustPackages.unknown.garage_table."0.8.1"; garage_table = rustPackages.unknown.garage_table."0.8.0";
garage_block = rustPackages.unknown.garage_block."0.8.1"; garage_block = rustPackages.unknown.garage_block."0.8.0";
garage_model = rustPackages.unknown.garage_model."0.8.1"; garage_model = rustPackages.unknown.garage_model."0.8.0";
garage_api = rustPackages.unknown.garage_api."0.8.1"; garage_api = rustPackages.unknown.garage_api."0.8.0";
garage_web = rustPackages.unknown.garage_web."0.8.1"; garage_web = rustPackages.unknown.garage_web."0.8.0";
garage = rustPackages.unknown.garage."0.8.1"; garage = rustPackages.unknown.garage."0.8.0";
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 [
[ "alloc" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "alloc")
[ "default" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "default")
[ "lazy_static" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "lazy_static")
[ "std" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "std")
]; ];
dependencies = { dependencies = {
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 "cfg_if" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { 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 "crossbeam_utils" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.8.8" { 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 "lazy_static" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { 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 "memoffset" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memoffset."0.6.5" { inherit profileName; }).out;
scopeguard = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".scopeguard."1.1.0" { 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;
}; };
buildDependencies = { buildDependencies = {
autocfg = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }).out; ${ 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;
}; };
}); });
@ -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 [
[ "default" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "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 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.isUnix then "libc" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { 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; ${ 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;
}; };
}); });
@ -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 = {
byteorder = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".byteorder."1.4.3" { inherit profileName; }).out; ${ 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;
}; };
}); });
"unknown".garage."0.8.1" = overridableMkRustCrate (profileName: rec { "unknown".garage."0.8.0" = overridableMkRustCrate (profileName: rec {
name = "garage"; name = "garage";
version = "0.8.1"; version = "0.8.0";
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.1" { inherit profileName; }).out; garage_api = (rustPackages."unknown".garage_api."0.8.0" { inherit profileName; }).out;
garage_block = (rustPackages."unknown".garage_block."0.8.1" { inherit profileName; }).out; garage_block = (rustPackages."unknown".garage_block."0.8.0" { inherit profileName; }).out;
garage_db = (rustPackages."unknown".garage_db."0.8.1" { inherit profileName; }).out; garage_db = (rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }).out;
garage_model = (rustPackages."unknown".garage_model."0.8.1" { inherit profileName; }).out; garage_model = (rustPackages."unknown".garage_model."0.8.0" { inherit profileName; }).out;
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.1" { inherit profileName; }).out; garage_rpc = (rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }).out;
garage_table = (rustPackages."unknown".garage_table."0.8.1" { inherit profileName; }).out; garage_table = (rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }).out;
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out; garage_util = (rustPackages."unknown".garage_util."0.8.0" { inherit profileName; }).out;
garage_web = (rustPackages."unknown".garage_web."0.8.1" { inherit profileName; }).out; garage_web = (rustPackages."unknown".garage_web."0.8.0" { 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.1" = overridableMkRustCrate (profileName: rec { "unknown".garage_api."0.8.0" = overridableMkRustCrate (profileName: rec {
name = "garage_api"; name = "garage_api";
version = "0.8.1"; version = "0.8.0";
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.1" { inherit profileName; }).out; garage_block = (rustPackages."unknown".garage_block."0.8.0" { inherit profileName; }).out;
garage_model = (rustPackages."unknown".garage_model."0.8.1" { inherit profileName; }).out; garage_model = (rustPackages."unknown".garage_model."0.8.0" { inherit profileName; }).out;
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.1" { inherit profileName; }).out; garage_rpc = (rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }).out;
garage_table = (rustPackages."unknown".garage_table."0.8.1" { inherit profileName; }).out; garage_table = (rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }).out;
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out; garage_util = (rustPackages."unknown".garage_util."0.8.0" { 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.1" = overridableMkRustCrate (profileName: rec { "unknown".garage_block."0.8.0" = overridableMkRustCrate (profileName: rec {
name = "garage_block"; name = "garage_block";
version = "0.8.1"; version = "0.8.0";
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.1" { inherit profileName; }).out; garage_db = (rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }).out;
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.1" { inherit profileName; }).out; garage_rpc = (rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }).out;
garage_table = (rustPackages."unknown".garage_table."0.8.1" { inherit profileName; }).out; garage_table = (rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }).out;
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out; garage_util = (rustPackages."unknown".garage_util."0.8.0" { 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,21 +1649,20 @@ in
}; };
}); });
"unknown".garage_db."0.8.1" = overridableMkRustCrate (profileName: rec { "unknown".garage_db."0.8.0" = overridableMkRustCrate (profileName: rec {
name = "garage_db"; name = "garage_db";
version = "0.8.1"; version = "0.8.0";
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")
[ "sled" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/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 = {
@ -1673,7 +1672,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;
sled = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sled."0.34.7" { 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;
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 = {
@ -1681,16 +1680,15 @@ in
}; };
}); });
"unknown".garage_model."0.8.1" = overridableMkRustCrate (profileName: rec { "unknown".garage_model."0.8.0" = overridableMkRustCrate (profileName: rec {
name = "garage_model"; name = "garage_model";
version = "0.8.1"; version = "0.8.0";
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")
[ "sled" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_model/sled") "sled")
(lib.optional (rootFeatures' ? "garage/sqlite" || rootFeatures' ? "garage_model/sqlite") "sqlite") (lib.optional (rootFeatures' ? "garage/sqlite" || rootFeatures' ? "garage_model/sqlite") "sqlite")
]; ];
dependencies = { dependencies = {
@ -1701,11 +1699,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.1" { inherit profileName; }).out; garage_block = (rustPackages."unknown".garage_block."0.8.0" { inherit profileName; }).out;
garage_db = (rustPackages."unknown".garage_db."0.8.1" { inherit profileName; }).out; garage_db = (rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }).out;
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.1" { inherit profileName; }).out; garage_rpc = (rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }).out;
garage_table = (rustPackages."unknown".garage_table."0.8.1" { inherit profileName; }).out; garage_table = (rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }).out;
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out; garage_util = (rustPackages."unknown".garage_util."0.8.0" { 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;
@ -1719,9 +1717,9 @@ in
}; };
}); });
"unknown".garage_rpc."0.8.1" = overridableMkRustCrate (profileName: rec { "unknown".garage_rpc."0.8.0" = overridableMkRustCrate (profileName: rec {
name = "garage_rpc"; name = "garage_rpc";
version = "0.8.1"; version = "0.8.0";
registry = "unknown"; registry = "unknown";
src = fetchCrateLocal (workspaceSrc + "/src/rpc"); src = fetchCrateLocal (workspaceSrc + "/src/rpc");
features = builtins.concatLists [ features = builtins.concatLists [
@ -1738,12 +1736,14 @@ in
arc_swap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }).out; arc_swap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".arc-swap."1.5.0" { inherit profileName; }).out;
async_trait = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }).out; async_trait = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.52" { profileName = "__noProfile"; }).out;
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;
bytesize = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytesize."1.1.0" { inherit profileName; }).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; ${ 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.1" { inherit profileName; }).out; garage_util = (rustPackages."unknown".garage_util."0.8.0" { 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;
itertools = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".itertools."0.10.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;
${ if rootFeatures' ? "garage/kubernetes-discovery" || rootFeatures' ? "garage_rpc/kube" || rootFeatures' ? "garage_rpc/kubernetes-discovery" then "kube" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube."0.75.0" { inherit profileName; }).out; ${ if rootFeatures' ? "garage/kubernetes-discovery" || rootFeatures' ? "garage_rpc/kube" || rootFeatures' ? "garage_rpc/kubernetes-discovery" then "kube" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".kube."0.75.0" { 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;
@ -1763,9 +1763,9 @@ in
}; };
}); });
"unknown".garage_table."0.8.1" = overridableMkRustCrate (profileName: rec { "unknown".garage_table."0.8.0" = overridableMkRustCrate (profileName: rec {
name = "garage_table"; name = "garage_table";
version = "0.8.1"; version = "0.8.0";
registry = "unknown"; registry = "unknown";
src = fetchCrateLocal (workspaceSrc + "/src/table"); src = fetchCrateLocal (workspaceSrc + "/src/table");
dependencies = { dependencies = {
@ -1773,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.1" { inherit profileName; }).out; garage_db = (rustPackages."unknown".garage_db."0.8.0" { inherit profileName; }).out;
garage_rpc = (rustPackages."unknown".garage_rpc."0.8.1" { inherit profileName; }).out; garage_rpc = (rustPackages."unknown".garage_rpc."0.8.0" { inherit profileName; }).out;
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out; garage_util = (rustPackages."unknown".garage_util."0.8.0" { 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;
@ -1788,9 +1788,9 @@ in
}; };
}); });
"unknown".garage_util."0.8.1" = overridableMkRustCrate (profileName: rec { "unknown".garage_util."0.8.0" = overridableMkRustCrate (profileName: rec {
name = "garage_util"; name = "garage_util";
version = "0.8.1"; version = "0.8.0";
registry = "unknown"; registry = "unknown";
src = fetchCrateLocal (workspaceSrc + "/src/util"); src = fetchCrateLocal (workspaceSrc + "/src/util");
features = builtins.concatLists [ features = builtins.concatLists [
@ -1805,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.1" { inherit profileName; }).out; garage_db = (rustPackages."unknown".garage_db."0.8.0" { 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;
@ -1825,18 +1825,18 @@ in
}; };
}); });
"unknown".garage_web."0.8.1" = overridableMkRustCrate (profileName: rec { "unknown".garage_web."0.8.0" = overridableMkRustCrate (profileName: rec {
name = "garage_web"; name = "garage_web";
version = "0.8.1"; version = "0.8.0";
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.1" { inherit profileName; }).out; garage_api = (rustPackages."unknown".garage_api."0.8.0" { inherit profileName; }).out;
garage_model = (rustPackages."unknown".garage_model."0.8.1" { inherit profileName; }).out; garage_model = (rustPackages."unknown".garage_model."0.8.0" { inherit profileName; }).out;
garage_table = (rustPackages."unknown".garage_table."0.8.1" { inherit profileName; }).out; garage_table = (rustPackages."unknown".garage_table."0.8.0" { inherit profileName; }).out;
garage_util = (rustPackages."unknown".garage_util."0.8.1" { inherit profileName; }).out; garage_util = (rustPackages."unknown".garage_util."0.8.0" { 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;
@ -2372,12 +2372,12 @@ 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 = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"; }; src = fetchCratesIo { inherit name version; sha256 = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"; };
features = builtins.concatLists [ features = builtins.concatLists [
(lib.optional (rootFeatures' ? "garage/opentelemetry-otlp" || rootFeatures' ? "garage/telemetry-otlp") "default") [ "default" ]
(lib.optional (rootFeatures' ? "garage/opentelemetry-otlp" || rootFeatures' ? "garage/telemetry-otlp") "use_alloc") [ "use_alloc" ]
(lib.optional (rootFeatures' ? "garage/opentelemetry-otlp" || rootFeatures' ? "garage/telemetry-otlp") "use_std") [ "use_std" ]
]; ];
dependencies = { dependencies = {
${ if rootFeatures' ? "garage/opentelemetry-otlp" || rootFeatures' ? "garage/telemetry-otlp" then "either" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".either."1.6.1" { inherit profileName; }).out; either = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".either."1.6.1" { inherit profileName; }).out;
}; };
}); });
@ -2450,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.1" { 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;
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;
@ -2848,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 [
[ "default" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "default")
]; ];
buildDependencies = { buildDependencies = {
autocfg = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }).out; ${ 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;
}; };
}); });
@ -4734,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 [
[ "default" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "default")
[ "no_metrics" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "no_metrics")
]; ];
dependencies = { dependencies = {
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 "crc32fast" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.3.2" { 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_epoch" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-epoch."0.9.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 "crossbeam_utils" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.8.8" { 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") && (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;
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 "fxhash" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fxhash."0.2.1" { 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 "libc" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.121" { 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 "log" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.16" { inherit profileName; }).out;
parking_lot = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking_lot."0.11.2" { 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;
}; };
}); });
@ -5398,6 +5398,7 @@ in
[ "attributes" ] [ "attributes" ]
[ "default" ] [ "default" ]
[ "log" ] [ "log" ]
[ "log-always" ]
[ "std" ] [ "std" ]
[ "tracing-attributes" ] [ "tracing-attributes" ]
]; ];
@ -5891,7 +5892,7 @@ in
[ "ntstatus" ] [ "ntstatus" ]
[ "objbase" ] [ "objbase" ]
[ "processenv" ] [ "processenv" ]
[ "processthreadsapi" ] (lib.optional (rootFeatures' ? "garage/default" || rootFeatures' ? "garage/sled" || rootFeatures' ? "garage_db/sled" || rootFeatures' ? "garage_model/sled") "processthreadsapi")
[ "profileapi" ] [ "profileapi" ]
[ "schannel" ] [ "schannel" ]
[ "securitybaseapi" ] [ "securitybaseapi" ]

View file

@ -11,14 +11,14 @@ let
build_debug_and_release = (target: { build_debug_and_release = (target: {
debug = (compile { debug = (compile {
inherit system target git_version pkgsSrc cargo2nixOverlay; inherit target git_version;
release = false; release = false;
}).workspace.garage { }).workspace.garage {
compileMode = "build"; compileMode = "build";
}; };
release = (compile { release = (compile {
inherit system target git_version pkgsSrc cargo2nixOverlay; inherit target git_version;
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 system git_version pkgsSrc cargo2nixOverlay; inherit git_version;
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 system git_version pkgsSrc cargo2nixOverlay; inherit git_version;
target = "x86_64-unknown-linux-musl"; target = "x86_64-unknown-linux-musl";
compiler = "clippy"; compiler = "clippy";
}).workspace.garage { }).workspace.garage {

View file

@ -1,17 +0,0 @@
# 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
```

View file

@ -1,59 +0,0 @@
/* 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+ */
}

View file

@ -1,24 +0,0 @@
<!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>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,54 +0,0 @@
+++
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)

View file

@ -1,69 +0,0 @@
+++
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)

View file

@ -1,55 +0,0 @@
+++
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)

View file

@ -1,138 +0,0 @@
+++
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)

View file

@ -1,47 +0,0 @@
+++
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*

View file

@ -1,5 +1,5 @@
+++ +++
title = "Existing integrations" title = "Integrations"
weight = 3 weight = 3
sort_by = "weight" sort_by = "weight"
template = "documentation.html" template = "documentation.html"
@ -14,6 +14,7 @@ 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

View file

@ -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) | ✅ | Supported with the website endpoint, proxifying private videos unsupported | | [Peertube](#peertube) | ✅ | Must be configured with the website endpoint |
| [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,10 +128,6 @@ 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
@ -199,11 +195,6 @@ 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'

View file

@ -1,10 +1,8 @@
+++ +++
title = "Others" title = "Your code (PHP, JS, Go...)"
weight = 99 weight = 30
+++ +++
## 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,
@ -15,14 +13,44 @@ 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)
### Java ## Javascript
- 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)
@ -32,18 +60,23 @@ 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)
### .NET ## Rust
- 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)

View file

@ -1,306 +0,0 @@
+++
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
```

View file

@ -11,9 +11,8 @@ 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:
@ -22,6 +21,10 @@ 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
@ -46,42 +49,6 @@ 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).
@ -109,12 +76,11 @@ 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.toml` for our cluster would look as follows: A valid `/etc/garage/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"
@ -124,6 +90,8 @@ 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"
@ -164,21 +132,6 @@ 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

View file

@ -1,6 +1,6 @@
+++ +++
title = "Recovering from failures" title = "Recovering from failures"
weight = 50 weight = 35
+++ +++
Garage is meant to work on old, second-hand hardware. Garage is meant to work on old, second-hand hardware.

View file

@ -70,16 +70,14 @@ 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;
# A "backup" server is only used if all others have failed. # you can assign weights if you have some servers
server garage-remote.example.com:3900 backup; # that are more powerful than others
# 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;
} }
@ -98,8 +96,6 @@ 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;
} }
} }
``` ```

View file

@ -1,6 +1,6 @@
+++ +++
title = "Upgrading Garage" title = "Upgrading Garage"
weight = 60 weight = 40
+++ +++
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.

View file

@ -1,6 +1,6 @@
+++ +++
title = "Design" title = "Design"
weight = 6 weight = 5
sort_by = "weight" sort_by = "weight"
template = "documentation.html" template = "documentation.html"
+++ +++

View file

@ -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 solution, we focus on the following **desirable properties**: Garage is an opinionated object storage solutoin, 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).

View file

@ -1,6 +1,6 @@
+++ +++
title = "Development" title = "Development"
weight = 7 weight = 6
sort_by = "weight" sort_by = "weight"
template = "documentation.html" template = "documentation.html"
+++ +++

View file

@ -42,25 +42,25 @@ you can [build Garage from source](@/documentation/cookbook/from-source.md).
## Configuring and starting Garage ## Configuring and starting Garage
### Generating a first configuration file ### Writing 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`).
We will create it with the following command line ```toml
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 = "$(openssl rand -hex 32)" rpc_secret = "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec"
bootstrap_peers = []
[s3_api] [s3_api]
s3_region = "garage" s3_region = "garage"
@ -71,26 +71,12 @@ 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
``` ```
Now that your configuration file has been created, you can put The `rpc_secret` value provided above is just an example. It will work, but in
it in the right place. By default, garage looks at **`/etc/garage.toml`.** order to secure your cluster you will need to use another one. You can generate
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
@ -233,7 +219,6 @@ 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
``` ```
@ -247,73 +232,54 @@ garage bucket info nextcloud-bucket
## Uploading and downlading from Garage ## Uploading and downlading from Garage
To download and upload files on garage, we can use a third-party tool named `awscli`. We recommend the use of MinIO Client to interact with Garage files (`mc`).
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).
### Install and configure `awscli` ### Configure `mc`
If you have python on your system, you can install it with: You need your access key and secret key created above.
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
python -m pip install --user awscli mc alias set \
my-garage \
http://127.0.0.1:3900 \
<access key> \
<secret key> \
--api S3v4
``` ```
Now that `awscli` is installed, you must configure it to talk to your Garage instance, ### Use `mc`
with your key. There are multiple ways to do that, the simplest one is to create a file
named `~/.awsrc` with this content: You can not list buckets from `mc` currently.
But the following commands and many more should work:
```bash ```bash
export AWS_ACCESS_KEY_ID=xxxx # put your Key ID here mc cp image.png my-garage/nextcloud-bucket
export AWS_SECRET_ACCESS_KEY=xxxx # put your Secret key here mc cp my-garage/nextcloud-bucket/image.png .
export AWS_DEFAULT_REGION='garage' mc ls my-garage/nextcloud-bucket
export AWS_ENDPOINT='http://localhost:3900' mc mirror localdir/ my-garage/another-bucket
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:
- [minio-client](@/documentation/connect/cli.md#minio-client) - the [AWS CLI](https://aws.amazon.com/cli/)
- [s3cmd](@/documentation/connect/cli.md#s3cmd) - [`rclone`](https://rclone.org/)
- [rclone](@/documentation/connect/cli.md#rclone) - [Cyberduck](https://cyberduck.io/)
- [Cyberduck](@/documentation/connect/cli.md#cyberduck) - [`s3cmd`](https://s3tools.org/s3cmd)
- [WinSCP](@/documentation/connect/cli.md#winscp)
An exhaustive list is maintained in the ["Integrations" > "Browsing tools" section](@/documentation/connect/_index.md). Refer to the ["Integrations" section](@/documentation/connect/_index.md) to learn how to
configure application and command line utilities to integrate with Garage.

View file

@ -1,6 +1,6 @@
+++ +++
title = "Reference Manual" title = "Reference Manual"
weight = 5 weight = 4
sort_by = "weight" sort_by = "weight"
template = "documentation.html" template = "documentation.html"
+++ +++

View file

@ -47,13 +47,598 @@ Returns internal Garage metrics in Prometheus format.
### Cluster operations ### Cluster operations
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). #### GetClusterStatus `GET /v0/status`
Requesting the API from the command line can be as simple as running: Returns the cluster's current status in JSON, including:
```bash - ID of the node being queried and its version of the Garage daemon
curl -H 'Authorization: Bearer s3cr3t' http://localhost:3903/v0/status | jq - 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"
]
}
}
}
}
``` ```
For more advanced use cases, we recommend using a SDK. #### ConnectClusterNodes `POST /v0/connect`
[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.

View file

@ -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.
Functionality included in the admin API include: setting up and monitoring Functionnality 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).

View file

@ -1,6 +1,6 @@
+++ +++
title = "Working Documents" title = "Working Documents"
weight = 8 weight = 7
sort_by = "weight" sort_by = "weight"
template = "documentation.html" template = "documentation.html"
+++ +++

View file

@ -1,6 +1,6 @@
+++ +++
title = "Design draft (obsolete)" title = "Design draft (obsolete)"
weight = 900 weight = 50
+++ +++
**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.

View file

@ -1,6 +1,6 @@
+++ +++
title = "Load balancing data (obsolete)" title = "Load balancing data (obsolete)"
weight = 910 weight = 60
+++ +++
**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.**

View file

@ -1,75 +0,0 @@
+++
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.

View file

@ -1,686 +0,0 @@
+++
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.

5
doc/optimal_layout_report/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
optimal_layout.aux
optimal_layout.log
optimal_layout.synctex.gz
optimal_layout.bbl
optimal_layout.blg

Binary file not shown.

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 270 KiB

View file

@ -0,0 +1,11 @@
@article{even1975network,
title={Network flow and testing graph connectivity},
author={Even, Shimon and Tarjan, R Endre},
journal={SIAM journal on computing},
volume={4},
number={4},
pages={507--518},
year={1975},
publisher={SIAM}
}

Binary file not shown.

View file

@ -0,0 +1,709 @@
\documentclass[]{article}
\usepackage{amsmath,amssymb}
\usepackage{amsthm}
\usepackage{graphicx,xcolor}
\usepackage{algorithm,algpseudocode,float}
\renewcommand\thesubsubsection{\Alph{subsubsection})}
\newtheorem{proposition}{Proposition}
%opening
\title{Optimal partition assignment in Garage}
\author{Mendes}
\begin{document}
\maketitle
\section{Introduction}
\subsection{Context}
Garage is an open-source distributed storage service blablabla$\dots$
Every object to be stored in the system falls in a partition given by the last $k$ bits of its hash. There are $P=2^k$ partitions. Every partition will be stored on distinct nodes of the system. The goal of the assignment of partitions to nodes is to ensure (nodes and zone) redundancy and to be as efficient as possible.
\subsection{Formal description of the problem}
We are given a set of nodes $\mathbf{N}$ and a set of zones $\mathbf{Z}$. Every node $n$ has a non-negative storage capacity $c_n\ge 0$ and belongs to a zone $z\in \mathbf{Z}$. We are also given a number of partition $P>0$ (typically $P=256$).
We would like to compute an assignment of nodes to partitions. We will impose some redundancy constraints to this assignment, and under these constraints, we want our system to have the largest storage capacity possible. To link storage capacity to partition assignment, we make the following assumption:
\begin{equation}
\tag{H1}
\text{\emph{All partitions have the same size $s$.}}
\end{equation}
This assumption is justified by the dispersion of the hashing function, when the number of partitions is small relative to the number of stored large objects.
Every node $n$ wille store some number $k_n$ of partitions. Hence the partitions stored by $n$ (and hence all partitions by our assumption) have there size bounded by $c_n/k_n$. This remark leads us to define the optimal size that we will want to maximize:
\begin{equation}
\label{eq:optimal}
\tag{OPT}
s^* = \min_{n \in N} \frac{c_n}{k_n}.
\end{equation}
When the capacities of the nodes are updated (this includes adding or removing a node), we want to update the assignment as well. However, transferring the data between nodes has a cost and we would like to limit the number of changes in the assignment. We make the following assumption:
\begin{equation}
\tag{H2}
\text{\emph{Updates of capacity happens rarely relatively to object storing.}}
\end{equation}
This assumption justifies that when we compute the new assignment, it is worth to optimize the partition size \eqref{eq:optimal} first, and then, among the possible optimal solution, to try to minimize the number of partition transfers.
For now, in the following, we ask the following redundancy constraint:
\textbf{Parametric node and zone redundancy:} Given two integer parameters $1\le \rho_\mathbf{Z} \le \rho_\mathbf{N}$, we ask every partition to be stored on $\rho_\mathbf{N}$ distinct nodes, and these nodes must belong to at least $\rho_\mathbf{Z}$ distinct zones.
\textbf{Mode 3-strict:} every partition needs to be assignated to three nodes belonging to three different zones.
\textbf{Mode 3:} every partition needs to be assignated to three nodes. We try to spread the three nodes over different zones as much as possible.
\textbf{Warning:} This is a working document written incrementaly. The last version of the algorithm is the \textbf{parametric assignment} described in the next section.
\section{Computation of a parametric assignment}
\textbf{Attention : }We change notations in this section.
Notations : let $P$ be the number of partitions, $N$ the number of nodes, $Z$ the number of zones. Let $\mathbf{P,N,Z}$ be the label sets of, respectively, partitions, nodes and zones.
Let $s^*$ be the largest partition size achievable with the redundancy constraints. Let $(c_n)_{n\in \mathbf{N}}$ be the storage capacity of every node.
In this section, we propose a third specification of the problem. The user inputs two redundancy parameters $1\le \rho_\mathbf{Z} \le \rho_\mathbf{N}$. We compute an assignment $\alpha = (\alpha_p^1, \ldots, \alpha_p^{\rho_\mathbf{N}})_{p\in \mathbf{P}}$ such that every partition $p$ is associated to $\rho_\mathbf{N}$ distinct nodes $\alpha_p^1, \ldots, \alpha_p^{\rho_\mathbf{N}}$ and these nodes belong to at least $\rho_\mathbf{Z}$ distinct zones.
If the layout contained a previous assignment $\alpha'$, we try to minimize the amount of data to transfer during the layout update by making $\alpha$ as close as possible to $\alpha'$.
In the following subsections, we describe the successive steps of the algorithm we propose to compute $\alpha$.
\subsubsection*{Algorithm}
\begin{algorithmic}[1]
\Function{Compute Layout}{$\mathbf{N}$, $\mathbf{Z}$, $\mathbf{P}$, $(c_n)_{n\in \mathbf{N}}$, $\rho_\mathbf{N}$, $\rho_\mathbf{Z}$, $\alpha'$}
\State $s^* \leftarrow$ \Call{Compute Partition Size}{$\mathbf{N}$, $\mathbf{Z}$, $\mathbf{P}$, $(c_n)_{n\in \mathbf{N}}$, $\rho_\mathbf{N}$, $\rho_\mathbf{Z}$}
\State $G \leftarrow G(s^*)$
\State $f \leftarrow$ \Call{Compute Candidate Assignment}{$G$, $\alpha'$}
\State $f^* \leftarrow$ \Call{Minimize transfer load}{$G$, $f$, $\alpha'$}
\State Build $\alpha^*$ from $f^*$
\State \Return $\alpha^*$
\EndFunction
\end{algorithmic}
\subsubsection*{Complexity}
As we will see in the next sections, the worst case complexity of this algorithm is $O(P^2 N^2)$. The minimization of transfer load is the most expensive step, and it can run with a timeout since it is only an optimization step. Without this step (or with a smart timeout), the worst cas complexity can be $O((PN)^{3/2}\log C)$ where $C$ is the total storage capacity of the cluster.
\subsection{Determination of the partition size $s^*$}
Again, we will represent an assignment $\alpha$ as a flow in a specific graph $G$. We will not compute the optimal partition size $s^*$ a priori, but we will determine it by dichotomy, as the largest size $s$ such that the maximal flow achievable on $G=G(s)$ has value $\rho_\mathbf{N}P$. We will assume that the capacities are given in a small enough unit (say, Megabytes), and we will determine $s^*$ at the precision of the given unit.
Given some candidate size value $s$, we describe the oriented weighted graph $G=(V,E)$ with vertex set $V$ arc set $E$.
The set of vertices $V$ contains the source $\mathbf{s}$, the sink $\mathbf{t}$, vertices
$\mathbf{p^+, p^-}$ for every partition $p$, vertices $\mathbf{x}_{p,z}$ for every partition $p$ and zone $z$, and vertices $\mathbf{n}$ for every node $n$.
The set of arcs $E$ contains:
\begin{itemize}
\item ($\mathbf{s}$,$\mathbf{p}^+$, $\rho_\mathbf{Z}$) for every partition $p$;
\item ($\mathbf{s}$,$\mathbf{p}^-$, $\rho_\mathbf{N}-\rho_\mathbf{Z}$) for every partition $p$;
\item ($\mathbf{p}^+$,$\mathbf{x}_{p,z}$, 1) for every partition $p$ and zone $z$;
\item ($\mathbf{p}^-$,$\mathbf{x}_{p,z}$, $\rho_\mathbf{N}-\rho_\mathbf{Z}$) for every partition $p$ and zone $z$;
\item ($\mathbf{x}_{p,z}$,$\mathbf{n}$, 1) for every partition $p$, zone $z$ and node $n\in z$;
\item ($\mathbf{n}$, $\mathbf{t}$, $\lfloor c_n/s \rfloor$) for every node $n$.
\end{itemize}
In the following complexity calculations, we will use the number of vertices and edges of $G$. Remark from now that $\# V = O(PZ)$ and $\# E = O(PN)$.
\begin{proposition}
An assignment $\alpha$ is realizable with partition size $s$ and the redundancy constraints $(\rho_\mathbf{N},\rho_\mathbf{Z})$ if and only if there exists a maximal flow function $f$ in $G$ with total flow $\rho_\mathbf{N}P$, such that the arcs ($\mathbf{x}_{p,z}$,$\mathbf{n}$, 1) used are exactly those for which $p$ is associated to $n$ in $\alpha$.
\end{proposition}
\begin{proof}
Given such flow $f$, we can reconstruct a candidate $\alpha$. In $f$, the flow passing through $\mathbf{p^+}$ and $\mathbf{p^-}$ is $\rho_\mathbf{N}$, and since the outgoing capacity of every $\mathbf{x}_{p,z}$ is 1, every partition is associated to $\rho_\mathbf{N}$ distinct nodes. The fraction $\rho_\mathbf{Z}$ of the flow passing through every $\mathbf{p^+}$ must be spread over as many distinct zones as every arc outgoing from $\mathbf{p^+}$ has capacity 1. So the reconstructed $\alpha$ verifies the redundancy constraints. For every node $n$, the flow between $\mathbf{n}$ and $\mathbf{t}$ corresponds to the number of partitions associated to $n$. By construction of $f$, this does not exceed $\lfloor c_n/s \rfloor$. We assumed that the partition size is $s$, hence this association does not exceed the storage capacity of the nodes.
In the other direction, given an assignment $\alpha$, one can similarly check that the facts that $\alpha$ respects the redundancy constraints, and the storage capacities of the nodes, are necessary condition to construct a maximal flow function $f$.
\end{proof}
\textbf{Implementation remark:} In the flow algorithm, while exploring the graph, we explore the neighbours of every vertex in a random order to heuristically spread the association between nodes and partitions.
\subsubsection*{Algorithm}
With this result mind, we can describe the first step of our algorithm. All divisions are supposed to be integer division.
\begin{algorithmic}[1]
\Function{Compute Partition Size}{$\mathbf{N}$, $\mathbf{Z}$, $\mathbf{P}$, $(c_n)_{n\in \mathbf{N}}$, $\rho_\mathbf{N}$, $\rho_\mathbf{Z}$}
\State Build the graph $G=G(s=1)$
\State $ f \leftarrow$ \Call{Maximal flow}{$G$}
\If{$f.\mathrm{total flow} < \rho_\mathbf{N}P$}
\State \Return Error: capacities too small or constraints too strong.
\EndIf
\State $s^- \leftarrow 1$
\State $s^+ \leftarrow 1+\frac{1}{\rho_\mathbf{N}}\sum_{n \in \mathbf{N}} c_n$
\While{$s^-+1 < s^+$}
\State Build the graph $G=G(s=(s^-+s^+)/2)$
\State $ f \leftarrow$ \Call{Maximal flow}{$G$}
\If{$f.\mathrm{total flow} < \rho_\mathbf{N}P$}
\State $s^+ \leftarrow (s^- + s^+)/2$
\Else
\State $s^- \leftarrow (s^- + s^+)/2$
\EndIf
\EndWhile
\State \Return $s^-$
\EndFunction
\end{algorithmic}
\subsubsection*{Complexity}
To compute the maximal flow, we use Dinic's algorithm. Its complexity on general graphs is $O(\#V^2 \#E)$, but on graphs with edge capacity bounded by a constant, it turns out to be $O(\#E^{3/2})$. The graph $G$ does not fall in this case since the capacities of the arcs incoming to $\mathbf{t}$ are far from bounded. However, the proof of this complexity works readily for graph where we only ask the edges \emph{not} incoming to the sink $\mathbf{t}$ to have their capacities bounded by a constant. One can find the proof of this claim in \cite[Section 2]{even1975network}.
The dichotomy adds a logarithmic factor $\log (C)$ where $C=\sum_{n \in \mathbf{N}} c_n$ is the total capacity of the cluster. The total complexity of this first function is hence
$O(\#E^{3/2}\log C ) = O\big((PN)^{3/2} \log C\big)$.
\subsubsection*{Metrics}
We can display the discrepancy between the computed $s^*$ and the best size we could hope for a given total capacity, that is $C/\rho_\mathbf{N}$.
\subsection{Computation of a candidate assignment}
Now that we have the optimal partition size $s^*$, to compute a candidate assignment, it would be enough to compute a maximal flow function $f$ on $G(s^*)$. This is what we do if there was no previous assignment $\alpha'$.
If there was some $\alpha'$, we add a step that will heuristically help to obtain a candidate $\alpha$ closer to $\alpha'$. to do so, we fist compute a flow function $\tilde{f}$ that uses only the partition-to-node association appearing in $\alpha'$. Most likely, $\tilde{f}$ will not be a maximal flow of $G(s^*)$. In Dinic's algorithm, we can start from a non maximal flow function and then discover improving paths. This is what we do in starting from $\tilde{f}$. The hope\footnote{This is only a hope, because one can find examples where the construction of $f$ from $\tilde{f}$ produces an assignment $\alpha$ that is not as close as possible to $\alpha'$.} is that the final flow function $f$ will tend to keep the associations appearing in $\tilde{f}$.
More formally, we construct the graph $G_{|\alpha'}$ from $G$ by removing all the arcs $(\mathbf{x}_{p,z},\mathbf{n}, 1)$ where $p$ is not associated to $n$ in $\alpha'$. We compute a maximal flow function $\tilde{f}$ in $G_{|\alpha'}$. $\tilde{f}$ is also a valid (most likely non maximal) flow function in $G$. We compute a maximal flow function $f$ on $G$ by starting Dinic's algorithm on $\tilde{f}$.
\subsubsection*{Algorithm}
\begin{algorithmic}[1]
\Function{Compute Candidate Assignment}{$G$, $\alpha'$}
\State Build the graph $G_{|\alpha'}$
\State $ \tilde{f} \leftarrow$ \Call{Maximal flow}{$G_{|\alpha'}$}
\State $ f \leftarrow$ \Call{Maximal flow from flow}{$G$, $\tilde{f}$}
\State \Return $f$
\EndFunction
\end{algorithmic}
\textbf{Remark:} The function ``Maximal flow'' can be just seen as the function ``Maximal flow from flow'' called with the zero flow function as starting flow.
\subsubsection*{Complexity}
From the consideration of the last section, we have the complexity of the Dinic's algorithm $O(\#E^{3/2}) = O((PN)^{3/2})$.
\subsubsection*{Metrics}
We can display the flow value of $\tilde{f}$, which is an upper bound of the distance between $\alpha$ and $\alpha'$. It might be more a Debug level display than Info.
\subsection{Minimization of the transfer load}
Now that we have a candidate flow function $f$, we want to modify it to make its associated assignment as close as possible to $\alpha'$. Denote by $f'$ the maximal flow associated to $\alpha'$, and let $d(f, f')$ be distance between the associated assignments\footnote{It is the number of arcs of type $(\mathbf{x}_{p,z},\mathbf{n})$ saturated in one flow and not in the other.}.
We want to build a sequence $f=f_0, f_1, f_2 \dots$ of maximal flows such that $d(f_i, \alpha')$ decreases as $i$ increases. The distance being a non-negative integer, this sequence of flow functions must be finite. We now explain how to find some improving $f_{i+1}$ from $f_i$.
For any maximal flow $f$ in $G$, we define the oriented weighted graph $G_f=(V, E_f)$ as follows. The vertices of $G_f$ are the same as the vertices of $G$. $E_f$ contains the arc $(v_1,v_2, w)$ between vertices $v_1,v_2\in V$ with weight $w$ if and only if the arc $(v_1,v_2)$ is not saturated in $f$ (i.e. $c(v_1,v_2)-f(v_1,v_2) \ge 1$, we also consider reversed arcs). The weight $w$ is:
\begin{itemize}
\item $-1$ if $(v_1,v_2)$ is of type $(\mathbf{x}_{p,z},\mathbf{n})$ or $(\mathbf{x}_{p,z},\mathbf{n})$ and is saturated in only one of the two flows $f,f'$;
\item $+1$ if $(v_1,v_2)$ is of type $(\mathbf{x}_{p,z},\mathbf{n})$ or $(\mathbf{x}_{p,z},\mathbf{n})$ and is saturated in either both or none of the two flows $f,f'$;
\item $0$ otherwise.
\end{itemize}
If $\gamma$ is a simple cycle of arcs in $G_f$, we define its weight $w(\gamma)$ as the sum of the weights of its arcs. We can add $+1$ to the value of $f$ on the arcs of $\gamma$, and by construction of $G_f$ and the fact that $\gamma$ is a cycle, the function that we get is still a valid flow function on $G$, it is maximal as it has the same flow value as $f$. We denote this new function $f+\gamma$.
\begin{proposition}
Given a maximal flow $f$ and a simple cycle $\gamma$ in $G_f$, we have $d(f+\gamma, f') - d(f,f') = w(\gamma)$.
\end{proposition}
\begin{proof}
Let $X$ be the set of arcs of type $(\mathbf{x}_{p,z},\mathbf{n})$. Then we can express $d(f,f')$ as
\begin{align*}
d(f,f') & = \#\{e\in X ~|~ f(e)\neq f'(e)\}
= \sum_{e\in X} 1_{f(e)\neq f'(e)} \\
& = \frac{1}{2}\big( \#X + \sum_{e\in X} 1_{f(e)\neq f'(e)} - 1_{f(e)= f'(e)} \big).
\end{align*}
We can express the cycle weight as
\begin{align*}
w(\gamma) & = \sum_{e\in X, e\in \gamma} - 1_{f(e)\neq f'(e)} + 1_{f(e)= f'(e)}.
\end{align*}
Remark that since we passed on unit of flow in $\gamma$ to construct $f+\gamma$, we have for any $e\in X$, $f(e)=f'(e)$ if and only if $(f+\gamma)(e) \neq f'(e)$.
Hence
\begin{align*}
w(\gamma) & = \frac{1}{2}(w(\gamma) + w(\gamma)) \\
&= \frac{1}{2} \Big(
\sum_{e\in X, e\in \gamma} - 1_{f(e)\neq f'(e)} + 1_{f(e)= f'(e)} \\
& \qquad +
\sum_{e\in X, e\in \gamma} 1_{(f+\gamma)(e)\neq f'(e)} + 1_{(f+\gamma)(e)= f'(e)}
\Big).
\end{align*}
Plugging this in the previous equation, we find that
$$d(f,f')+w(\gamma) = d(f+\gamma, f').$$
\end{proof}
This result suggests that given some flow $f_i$, we just need to find a negative cycle $\gamma$ in $G_{f_i}$ to construct $f_{i+1}$ as $f_i+\gamma$. The following proposition ensures that this greedy strategy reaches an optimal flow.
\begin{proposition}
For any maximal flow $f$, $G_f$ contains a negative cycle if and only if there exists a maximal flow $f^*$ in $G$ such that $d(f^*, f') < d(f, f')$.
\end{proposition}
\begin{proof}
Suppose that there is such flow $f^*$. Define the oriented multigraph $M_{f,f^*}=(V,E_M)$ with the same vertex set $V$ as in $G$, and for every $v_1,v_2 \in V$, $E_M$ contains $(f^*(v_1,v_2) - f(v_1,v_2))_+$ copies of the arc $(v_1,v_2)$. For every vertex $v$, its total degree (meaning its outer degree minus its inner degree) is equal to
\begin{align*}
\deg v & = \sum_{u\in V} (f^*(v,u) - f(v,u))_+ - \sum_{u\in V} (f^*(u,v) - f(u,v))_+ \\
& = \sum_{u\in V} f^*(v,u) - f(v,u) = \sum_{u\in V} f^*(v,u) - \sum_{u\in V} f(v,u).
\end{align*}
The last two sums are zero for any inner vertex since $f,f^*$ are flows, and they are equal on the source and sink since the two flows are both maximal and have hence the same value. Thus, $\deg v = 0$ for every vertex $v$.
This implies that the multigraph $M_{f,f^*}$ is the union of disjoint simple cycles. $f$ can be transformed into $f^*$ by pushing a mass 1 along all these cycles in any order. Since $d(f^*, f')<d(f,f')$, there must exists one of these simple cycles $\gamma$ with $d(f+\gamma, f') < d(f, f')$. Finally, since we can push a mass in $f$ along $\gamma$, it must appear in $G_f$. Hence $\gamma$ is a cycle of $G_f$ with negative weight.
\end{proof}
In the next section we describe the corresponding algorithm. Instead of discovering only one cycle, we are allowed to discover a set $\Gamma$ of disjoint negative cycles.
\subsubsection*{Algorithm}
\begin{algorithmic}[1]
\Function{Minimize transfer load}{$G$, $f$, $\alpha'$}
\State Build the graph $G_f$
\State $\Gamma \leftarrow$ \Call{Detect Negative Cycles}{$G_f$}
\While{$\Gamma \neq \emptyset$}
\ForAll{$\gamma \in \Gamma$}
\State $f \leftarrow f+\gamma$
\EndFor
\State Update $G_f$
\State $\Gamma \leftarrow$ \Call{Detect Negative Cycles}{$G_f$}
\EndWhile
\State \Return $f$
\EndFunction
\end{algorithmic}
\subsubsection*{Complexity}
The distance $d(f,f')$ is bounded by the maximal number of differences in the associated assignment. If these assignment are totally disjoint, this distance is $2\rho_N P$. At every iteration of the While loop, the distance decreases, so there is at most $O(\rho_N P) = O(P)$ iterations.
The detection of negative cycle is done with the Bellman-Ford algorithm, whose complexity should normally be $O(\#E\#V)$. In our case, it amounts to $O(P^2ZN)$. Multiplied by the complexity of the outer loop, it amounts to $O(P^3ZN)$ which is a lot when the number of partitions and nodes starts to be large. To avoid that, we adapt the Bellman-Ford algorithm.
The Bellman-Ford algorithm runs $\#V$ iterations of an outer loop, and an inner loop over $E$. The idea is to compute the shortest paths from a source vertex $v$ to all other vertices. After $k$ iterations of the outer loop, the algorithm has computed all shortest path of length at most $k$. All simple paths have length at most $\#V-1$, so if there is an update in the last iteration of the loop, it means that there is a negative cycle in the graph. The observation that will enable us to improve the complexity is the following:
\begin{proposition}
In the graph $G_f$ (and $G$), all simple paths have a length at most $4N$.
\end{proposition}
\begin{proof}
Since $f$ is a maximal flow, there is no outgoing edge from $\mathbf{s}$ in $G_f$. One can thus check than any simple path of length 4 must contain at least two node of type $\mathbf{n}$. Hence on a path, at most 4 arcs separate two successive nodes of type $\mathbf{n}$.
\end{proof}
Thus, in the absence of negative cycles, shortest paths in $G_f$ have length at most $4N$. So we can do only $4N+1$ iterations of the outer loop in Bellman-Ford algorithm. This makes the complexity of the detection of one set of cycle to be $O(N\#E) = O(N^2 P)$.
With this improvement, the complexity of the whole algorithm is, in the worst case, $O(N^2P^2)$. However, since we detect several cycles at once and we start with a flow that might be close to the previous one, the number of iterations of the outer loop might be smaller in practice.
\subsubsection*{Metrics}
We can display the node and zone utilization ratio, by dividing the flow passing through them divided by their outgoing capacity. In particular, we can pinpoint saturated nodes and zones (i.e. used at their full potential).
We can display the distance to the previous assignment, and the number of partition transfers.
\section{Properties of an optimal 3-strict assignment}
\subsection{Optimal assignment}
\label{sec:opt_assign}
For every zone $z\in Z$, define the zone capacity $c_z = \sum_{v, z_v=z} c_v$ and define $C = \sum_v c_v = \sum_z c_z$.
One can check that the best we could be doing to maximize $s^*$ would be to use the nodes proportionally to their capacity. This would yield $s^*=C/(3N)$. This is not possible because of (i) redundancy constraints and (ii) integer rounding but it gives and upper bound.
\subsubsection*{Optimal utilization}
We call an \emph{utilization} a collection of non-negative integers $(n_v)_{v\in V}$ such that $\sum_v n_v = 3N$ and for every zone $z$, $\sum_{v\in z} n_v \le N$. We call such utilization \emph{optimal} if it maximizes $s^*$.
We start by computing a node sub-utilization $(\hat{n}_v)_{v\in V}$ such that for every zone $z$, $\sum_{v\in z} \hat{n}_v \le N$ and we show that there is an optimal utilization respecting the constraints and such that $\hat{n}_v \le n_v$ for every node.
Assume that there is a zone $z_0$ such that $c_{z_0}/C \ge 1/3$. Then for any $v\in z_0$, we define
$$\hat{n}_v = \left\lfloor\frac{c_v}{c_{z_0}}N\right\rfloor.$$
This choice ensures for any such $v$ that
$$
\frac{c_v}{\hat{n}_v} \ge \frac{c_{z_0}}{N} \ge \frac{C}{3N}
$$
which is the universal upper bound on $s^*$. Hence any optimal utilization $(n_v)$ can be modified to another optimal utilization such that $n_v\ge \hat{n}_v$
Because $z_0$ cannot store more than $N$ partition occurences, in any assignment, at least $2N$ partitions must be assignated to the zones $Z\setminus\{z_0\}$. Let $C_0 = C-c_{z_0}$. Suppose that there exists a zone $z_1\neq z_0$ such that $c_{z_1}/C_0 \ge 1/2$. Then, with the same argument as for $z_0$, we can define
$$\hat{n}_v = \left\lfloor\frac{c_v}{c_{z_1}}N\right\rfloor$$
for every $v\in z_1$.
Now we can assign the remaining partitions. Let $(\hat{N}, \hat{C})$ to be
\begin{itemize}
\item $(3N,C)$ if we did not find any $z_0$;
\item $(2N,C-c_{z_0})$ if there was a $z_0$ but no $z_1$;
\item $(N,C-c_{z_0}-c_{z_1})$ if there was a $z_0$ and a $z_1$.
\end{itemize}
Then at least $\hat{N}$ partitions must be spread among the remaining zones. Hence $s^*$ is upper bounded by $\hat{C}/\hat{N}$ and without loss of generality, we can define, for every node that is not in $z_0$ nor $z_1$,
$$\hat{n}_v = \left\lfloor\frac{c_v}{\hat{C}}\hat{N}\right\rfloor.$$
We constructed a sub-utilization $\hat{n}_v$. Now notice that $3N-\sum_v \hat{n}_v \le \# V$ where $\# V$ denotes the number of nodes. We can iteratively pick a node $v^*$ such that
\begin{itemize}
\item $\sum_{v\in z_{v^*}} \hat{n}_v < N$ where $z_{v^*}$ is the zone of $v^*$;
\item $v^*$ maximizes the quantity $c_v/(\hat{n}_v+1)$ among the vertices satisfying the first condition (i.e. not in a saturated zone).
\end{itemize}
We iterate these instructions until $\sum_v \hat{n}_v= 3N$, and at this stage we define $(n_v) = (\hat{n}_v)$. It is easy to prove by induction that at every step, there is an optimal utilization that is pointwise larger than $\hat{n}_v$, and in particular, that $(n_v)$ is optimal.
\subsubsection*{Existence of an optimal assignment}
As for now, the \emph{optimal utilization} that we obtained is just a vector of numbers and it is not clear that it can be realized as the utilization of some concrete assignment. Here is a way to get a concrete assignment.
Define $3N$ tokens $t_1,\ldots, t_{3N}\in V$ as follows:
\begin{itemize}
\item Enumerate the zones $z$ of $Z$ in any order;
\item enumerate the nodes $v$ of $z$ in any order;
\item repeat $n_v$ times the token $v$.
\end{itemize}
Then for $1\le i \le N$, define the triplet $T_i$ to be
$(t_i, t_{i+N}, t_{i+2N})$. Since the same nodes of a zone appear contiguously, the three nodes of a triplet must belong to three distinct zones.
However simple, this solution to go from an utilization to an assignment has the drawback of not spreading the triplets: a node will tend to be associated to the same two other nodes for many partitions. Hence, during data transfer, it will tend to use only two link, instead of spreading the bandwith use over many other links to other nodes. To achieve this goal, we will reframe the search of an assignment as a flow problem. and in the flow algorithm, we will introduce randomness in the order of exploration. This will be sufficient to obtain a good dispersion of the triplets.
\begin{figure}
\centering
\includegraphics[width=0.9\linewidth]{figures/naive}
\caption{On the left, the creation of a concrete assignment with the naive approach of repeating tokens. On the right, the zones containing the nodes.}
\end{figure}
\subsubsection*{Assignment as a maximum flow problem}
We describe the flow problem via its graph $(X,E)$ where $X$ is a set of vertices, and $E$ are directed weighted edges between the vertices. For every zone $z$, define $n_z=\sum_{v\in z} n_v$.
The set of vertices $X$ contains the source $\mathbf{s}$ and the sink $\mathbf{t}$; a vertex $\mathbf{x}_z$ for every zone $z\in Z$, and a vertex $\mathbf{y}_i$ for every partition index $1\le i\le N$.
The set of edges $E$ contains
\begin{itemize}
\item the edge $(\mathbf{s}, \mathbf{x}_z, n_z)$ for every zone $z\in Z$;
\item the edge $(\mathbf{x}_z, \mathbf{y}_i, 1)$ for every zone $z\in Z$ and partition $1\le i\le N$;
\item the edge $(\mathbf{y}_i, \mathbf{t}, 3)$ for every partition $1\le i\le N$.
\end{itemize}
\begin{figure}[b]
\centering
\includegraphics[width=0.6\linewidth]{figures/flow}
\caption{Flow problem to compute and optimal assignment.}
\end{figure}
We first show the equivalence between this problem and and the construction of an assignment. Given some optimal assignment $(n_v)$, define the flow $f:E\to \mathbb{N}$ that saturates every edge from $\mathbf{s}$ or to $\mathbf{t}$, takes value $1$ on the edge between $\mathbf{x}_z$ and $\mathbf{y}_i$ if partition $i$ is stored in some node of the zone $z$, and $0$ otherwise. One can easily check that $f$ thus defined is indeed a flow and is maximum.
Reciprocally, by the existence of maximum flows constructed from optimal assignments, any maximum flow must saturate the edges linked to the source or the sink. It can only take value 0 or 1 on the other edge, and every partition vertex is associated to exactly three distinct zone vertices. Every zone is associated to exactly $n_z$ partitions.
A maximum flow can be constructed using, for instance, Dinic's algorithm. This algorithm works by discovering augmenting path to iteratively increase the flow. During the exploration of the graph to find augmenting path, we can shuffle the order of enumeration of the neighbours to spread the associations between zones and partitions.
Once we have such association, we can randomly distribute the $n_z$ edges picked for every zone $z$ to its nodes $v\in z$ such that every such $v$ gets $n_z$ edges. This defines an optimal assignment of partitions to nodes.
\subsection{Minimal transfer}
Assume that there was a previous assignment $(T'_i)_{1\le i\le N}$ corresponding to utilizations $(n'_v)_{v\in V}$. We would like the new computed assignment $(T_i)_{1\le i\le N}$ from some $(n_v)_{v\in V}$ to minimize the number of partitions that need to be transferred. We can imagine two different objectives corresponding to different hypotheses:
\begin{equation}
\tag{H3A}
\label{hyp:A}
\text{\emph{Transfers between different zones cost much more than inside a zone.}}
\end{equation}
\begin{equation}
\tag{H3B}
\label{hyp:B}
\text{\emph{Changing zone is not the largest cost when transferring a partition.}}
\end{equation}
In case $A$, our goal will be to minimize the number of changes of zone in the assignment of partitions to zone. More formally, we will maximize the quantity
$$
Q_Z :=
\sum_{1\le i\le N}
\#\{z\in Z ~|~ z\cap T_i \neq \emptyset, z\cap T'_i \neq \emptyset \}
.$$
In case $B$, our goal will be to minimize the number of changes of nodes in the assignment of partitions to nodes. We will maximize the quantity
$$
Q_V :=
\sum_{1\le i\le N} \#(T_i \cap T'_i).
$$
It is tempting to hope that there is a way to maximize both quantity, that having the least discrepancy in terms of nodes will lead to the least discrepancy in terms of zones. But this is actually wrong! We propose the following counter-example to convince the reader:
We consider eight nodes $a, a', b, c, d, d', e, e'$ belonging to five different zones $\{a,a'\}, \{b\}, \{c\}, \{d,d'\}, \{e, e'\}$. We take three partitions ($N=3$), that are originally assigned with some utilization $(n'_v)_{v\in V}$ as follows:
$$
T'_1=(a,b,c) \qquad
T'_2=(a',b,d) \qquad
T'_3=(b,c,e).
$$
This assignment, with updated utilizations $(n_v)_{v\in V}$ minimizes the number of zone changes:
$$
T_1=(d,b,c) \qquad
T_2=(a,b,d) \qquad
T_3=(b,c,e').
$$
This one, with the same utilization, minimizes the number of node changes:
$$
T_1=(a,b,c) \qquad
T_2=(e',b,d) \qquad
T_3=(b,c,d').
$$
One can check that in this case, it is impossible to minimize both the number of zone and node changes.
Because of the redundancy constraint, we cannot use a greedy algorithm to just replace nodes in the triplets to try to get the new utilization rate: this could lead to blocking situation where there is still a hole to fill in a triplet but no available node satisfies the zone separation constraint. To circumvent this issue, we propose an algorithm based on finding cycles in a graph encoding of the assignment. As in section \ref{sec:opt_assign}, we can explore the neigbours in a random order in the graph algorithms, to spread the triplets distribution.
\subsubsection{Minimizing the zone discrepancy}
First, notice that, given an assignment of partitions to \emph{zones}, it is easy to deduce an assignment to \emph{nodes} that minimizes the number of transfers for this zone assignment: For every zone $z$ and every node $v\in z$, pick in any way a set $P_v$ of partitions that where assigned to $v$ in $T'$, to $z_v$ in $T$, with the cardinality of $P_v$ smaller than $n_v$. Once all these sets are chosen, complement the assignment to reach the right utilization for every node. If $\#P_v > n_v$, it means that all the partitions that could stay in $v$ (i.e. that were already in $v$ and are still assigned to its zone) do stay in $v$. If $\#P_v = n_v$, then $n_v$ partitions stay in $v$, which is the number of partitions that need to be in $v$ in the end. In both cases, we could not hope for better given the partition to zone assignment.
Our goal now is to find a assignment of partitions to zones that minimizes the number of zone transfers. To do so we are going to represent an assignment as a graph.
Let $G_T=(X,E_T)$ be the directed weighted graph with vertices $(\mathbf{x}_i)_{1\le i\le N}$ and $(\mathbf{y}_z)_{z\in Z}$. For any $1\le i\le N$ and $z\in Z$, $E_T$ contains the arc:
\begin{itemize}
\item $(\mathbf{x}_i, \mathbf{y}_z, +1)$, if $z$ appears in $T_i'$ and $T_i$;
\item $(\mathbf{x}_i, \mathbf{y}_z, -1)$, if $z$ appears in $T_i$ but not in $T'_i$;
\item $(\mathbf{y}_z, \mathbf{x}_i, -1)$, if $z$ appears in $T'_i$ but not in $T_i$;
\item $(\mathbf{y}_z, \mathbf{x}_i, +1)$, if $z$ does not appear in $T'_i$ nor in $T_i$.
\end{itemize}
In other words, the orientation of the arc encodes whether partition $i$ is stored in zone $z$ in the assignment $T$ and the weight $\pm 1$ encodes whether this corresponds to what happens in the assignment $T'$.
\begin{figure}[t]
\centering
\begin{minipage}{.40\linewidth}
\centering
\includegraphics[width=.8\linewidth]{figures/mini_zone}
\end{minipage}
\begin{minipage}{.55\linewidth}
\centering
\includegraphics[width=.8\linewidth]{figures/mini_node}
\end{minipage}
\caption{On the left: the graph $G_T$ encoding an assignment to minimize the zone discrepancy. On the right: the graph $G_T$ encoding an assignment to minimize the node discrepancy.}
\end{figure}
Notice that at every partition, there are three outgoing arcs, and at every zone, there are $n_z$ incoming arcs. Moreover, if $w(e)$ is the weight of an arc $e$, define the weight of $G_T$ by
\begin{align*}
w(G_T) := \sum_{e\in E} w(e) &= \#Z \times N - 4 \sum_{1\le i\le N} \#\{z\in Z ~|~ z\cap T_i = \emptyset, z\cap T'_i \neq \emptyset\} \\
&=\#Z \times N - 4 \sum_{1\le i\le N} 3- \#\{z\in Z ~|~ z\cap T_i \neq \emptyset, z\cap T'_i \neq \emptyset\} \\
&= (\#Z-12)N + 4 Q_Z.
\end{align*}
Hence maximizing $Q_Z$ is equivalent to maximizing $w(G_T)$.
Assume that their exist some assignment $T^*$ with the same utilization $(n_v)_{v\in V}$. Define $G_{T^*}$ similarly and consider the set $E_\mathrm{Diff} = E_T \setminus E_{T^*}$ of arcs that appear only in $G_T$. Since all vertices have the same number of incoming arcs in $G_T$ and $G_{T^*}$, the vertices of the graph $(X, E_\mathrm{Diff})$ must all have the same number number of incoming and outgoing arrows. So $E_\mathrm{Diff}$ can be expressed as a union of disjoint cycles. Moreover, the edges of $E_\mathrm{Diff}$ must appear in $E_{T^*}$ with reversed orientation and opposite weight. Hence, we have
$$
w(G_T) - w(G_{T^*}) = 2 \sum_{e\in E_\mathrm{Diff}} w(e).
$$
Hence, if $T$ is not optimal, there exists some $T^*$ with $w(G_T) < w(G_{T^*})$, and by the considerations above, there must exist a cycle in $E_\mathrm{Diff}$, and hence in $G_T$, with negative weight. If we reverse the edges and weights along this cycle, we obtain some graph. Since we did not change the incoming degree of any vertex, this is the graph encoding of some valid assignment $T^+$ such that $w(G_{T^+}) > w(G_T)$. We can iterate this operation until there is no other assignment $T^*$ with larger weight, that is until we obtain an optimal assignment.
\subsubsection{Minimizing the node discrepancy}
We will follow an approach similar to the one where we minimize the zone discrepancy. Here we will directly obtain a node assignment from a graph encoding.
Let $G_T=(X,E_T)$ be the directed weighted graph with vertices $(\mathbf{x}_i)_{1\le i\le N}$, $(\mathbf{y}_{z,i})_{z\in Z, 1\le i\le N}$ and $(\mathbf{u}_v)_{v\in V}$. For any $1\le i\le N$ and $z\in Z$, $E_T$ contains the arc:
\begin{itemize}
\item $(\mathbf{x}_i, \mathbf{y}_{z,i}, 0)$, if $z$ appears in $T_i$;
\item $(\mathbf{y}_{z,i}, \mathbf{x}_i, 0)$, if $z$ does not appear in $T_i$.
\end{itemize}
For any $1\le i\le N$ and $v\in V$, $E_T$ contains the arc:
\begin{itemize}
\item $(\mathbf{y}_{z_v,i}, \mathbf{u}_v, +1)$, if $v$ appears in $T_i'$ and $T_i$;
\item $(\mathbf{y}_{z_v,i}, \mathbf{u}_v, -1)$, if $v$ appears in $T_i$ but not in $T'_i$;
\item $(\mathbf{u}_v, \mathbf{y}_{z_v,i}, -1)$, if $v$ appears in $T'_i$ but not in $T_i$;
\item $(\mathbf{u}_v, \mathbf{y}_{z_v,i}, +1)$, if $v$ does not appear in $T'_i$ nor in $T_i$.
\end{itemize}
Every vertex $\mathbb{x}_i$ has outgoing degree 3, every vertex $\mathbf{y}_{z,v}$ has outgoing degree 1, and every vertex $\mathbf{u}_v$ has incoming degree $n_v$.
Remark that any graph respecting these degree constraints is the encoding of a valid assignment with utilizations $(n_v)_{v\in V}$, in particular no partition is stored in two nodes of the same zone.
We define $w(G_T)$ similarly:
\begin{align*}
w(G_T) := \sum_{e\in E_T} w(e) &= \#V \times N - 4\sum_{1\le i\le N} 3-\#(T_i\cap T'_i) \\
&= (\#V-12)N + 4Q_V.
\end{align*}
Exactly like in the previous section, the existence of an assignment with larger weight implies the existence of a negatively weighted cycle in $G_T$. Reversing this cycle gives us the encoding of a valid assignment with a larger weight. Iterating this operation yields an optimal assignment.
\subsubsection{Linear combination of both criteria}
In the graph $G_T$ defined in the previous section, instead of having weights $0$ and $\pm 1$, we could be having weights $\pm\alpha$ between $\mathbf{x}$ and $\mathbf{y}$ vertices, and weights $\pm\beta$ between $\mathbf{y}$ and $\mathbf{u}$ vertices, for some $\alpha,\beta>0$ (we have positive weight if the assignment corresponds to $T'$ and negative otherwise). Then
\begin{align*}
w(G_T) &= \sum_{e\in E_T} w(e) =
\alpha \big( (\#Z-12)N + 4 Q_Z\big) +
\beta \big( (\#V-12)N + 4 Q_V\big) \\
&= \mathrm{const}+ 4(\alpha Q_Z + \beta Q_V).
\end{align*}
So maximizing the weight of such graph encoding would be equivalent to maximizing a linear combination of $Q_Z$ and $Q_V$.
\subsection{Algorithm}
We give a high level description of the algorithm to compute an optimal 3-strict assignment. The operations appearing at lines 1,2,4 are respectively described by Algorithms \ref{alg:util},\ref{alg:opt} and \ref{alg:mini}.
\begin{algorithm}[H]
\caption{Optimal 3-strict assignment}
\label{alg:total}
\begin{algorithmic}[1]
\Function{Optimal 3-strict assignment}{$N$, $(c_v)_{v\in V}$, $T'$}
\State $(n_v)_{v\in V} \leftarrow$ \Call{Compute optimal utilization}{$N$, $(c_v)_{v\in V}$}
\State $(T_i)_{1\le i\le N} \leftarrow$ \Call{Compute candidate assignment}{$N$, $(n_v)_{v\in V}$}
\If {there was a previous assignment $T'$}
\State $T \leftarrow$ \Call{Minimization of transfers}{$(T_i)_{1\le i\le N}$, $(T'_i)_{1\le i\le N}$}
\EndIf
\State \Return $T$.
\EndFunction
\end{algorithmic}
\end{algorithm}
We give some considerations of worst case complexity for these algorithms. In the following, we assume $N>\#V>\#Z$. The complexity of Algorithm \ref{alg:total} is $O(N^3\# Z)$ if we assume \eqref{hyp:A} and $O(N^3 \#Z \#V)$ if we assume \eqref{hyp:B}.
Algorithm \ref{alg:util} can be implemented with complexity $O(\#V^2)$. The complexity of the function call at line \ref{lin:subutil} is $O(\#V)$. The difference between the sum of the subutilizations and $3N$ is at most the sum of the rounding errors when computing the $\hat{n}_v$. Hence it is bounded by $\#V$ and the loop at line \ref{lin:loopsub} is iterated at most $\#V$ times. Finding the minimizing $v$ at line \ref{lin:findmin} takes $O(\#V)$ operations (naively, we could also use a heap).
Algorithm \ref{alg:opt} can be implemented with complexity $O(N^3\times \#Z)$. The flow graph has $O(N+\#Z)$ vertices and $O(N\times \#Z)$ edges. Dinic's algorithm has complexity $O(\#\mathrm{Vertices}^2\#\mathrm{Edges})$ hence in our case it is $O(N^3\times \#Z)$.
Algorithm \ref{alg:mini} can be implented with complexity $O(N^3\# Z)$ under \eqref{hyp:A} and $O(N^3 \#Z \#V)$ under \eqref{hyp:B}.
The graph $G_T$ has $O(N)$ vertices and $O(N\times \#Z)$ edges under assumption \eqref{hyp:A} and respectively $O(N\times \#Z)$ vertices and $O(N\times \#V)$ edges under assumption \eqref{hyp:B}. The loop at line \ref{lin:repeat} is iterated at most $N$ times since the distance between $T$ and $T'$ decreases at every iteration. Bellman-Ford algorithm has complexity $O(\#\mathrm{Vertices}\#\mathrm{Edges})$, which in our case amounts to $O(N^2\# Z)$ under \eqref{hyp:A} and $O(N^2 \#Z \#V)$ under \eqref{hyp:B}.
\begin{algorithm}
\caption{Computation of the optimal utilization}
\label{alg:util}
\begin{algorithmic}[1]
\Function{Compute optimal utilization}{$N$, $(c_v)_{v\in V}$}
\State $(\hat{n}_v)_{v\in V} \leftarrow $ \Call{Compute subutilization}{$N$, $(c_v)_{v\in V}$} \label{lin:subutil}
\While{$\sum_{v\in V} \hat{n}_v < 3N$} \label{lin:loopsub}
\State Pick $v\in V$ minimizing $\frac{c_v}{\hat{n}_v+1}$ and such that
$\sum_{v'\in z_v} \hat{n}_{v'} < N$ \label{lin:findmin}
\State $\hat{n}_v \leftarrow \hat{n}_v+1$
\EndWhile
\State \Return $(\hat{n}_v)_{v\in V}$
\EndFunction
\State
\Function{Compute subutilization}{$N$, $(c_v)_{v\in V}$}
\State $R \leftarrow 3$
\For{$v\in V$}
\State $\hat{n}_v \leftarrow \mathrm{unset}$
\EndFor
\For{$z\in Z$}
\State $c_z \leftarrow \sum_{v\in z} c_v$
\EndFor
\State $C \leftarrow \sum_{z\in Z} c_z$
\While{$\exists z \in Z$ such that $R\times c_{z} > C$}
\For{$v\in z$}
\State $\hat{n}_v \leftarrow \left\lfloor \frac{c_v}{c_z} N \right\rfloor$
\EndFor
\State $C \leftarrow C-c_z$
\State $R\leftarrow R-1$
\EndWhile
\For{$v\in V$}
\If{$\hat{n}_v = \mathrm{unset}$}
\State $\hat{n}_v \leftarrow \left\lfloor \frac{Rc_v}{C} N \right\rfloor$
\EndIf
\EndFor
\State \Return $(\hat{n}_v)_{v\in V}$
\EndFunction
\end{algorithmic}
\end{algorithm}
\begin{algorithm}
\caption{Computation of a candidate assignment}
\label{alg:opt}
\begin{algorithmic}[1]
\Function{Compute candidate assignment}{$N$, $(n_v)_{v\in V}$}
\State Compute the flow graph $G$
\State Compute the maximal flow $f$ using Dinic's algorithm with randomized neighbours enumeration
\State Construct the assignment $(T_i)_{1\le i\le N}$ from $f$
\State \Return $(T_i)_{1\le i\le N}$
\EndFunction
\end{algorithmic}
\end{algorithm}
\begin{algorithm}
\caption{Minimization of the number of transfers}
\label{alg:mini}
\begin{algorithmic}[1]
\Function{Minimization of transfers}{$(T_i)_{1\le i\le N}$, $(T'_i)_{1\le i\le N}$}
\State Construct the graph encoding $G_T$
\Repeat \label{lin:repeat}
\State Find a negative cycle $\gamma$ using Bellman-Ford algorithm on $G_T$
\State Reverse the orientations and weights of edges in $\gamma$
\Until{no negative cycle is found}
\State Update $(T_i)_{1\le i\le N}$ from $G_T$
\State \Return $(T_i)_{1\le i\le N}$
\EndFunction
\end{algorithmic}
\end{algorithm}
\newpage
\section{Computation of a 3-non-strict assignment}
\subsection{Choices of optimality}
In this mode, we primarily want to store every partition on three nodes, and only secondarily try to spread the nodes among different zone. So we make the choice of not taking the zone repartition in the criterion of optimality.
We try to maximize $s^*$ defined in \eqref{eq:optimal}. So we can compute the optimal utilizations $(n_v)_{v\in V}$ with the only constraint that $n_v \le N$ for every node $v$. As in the previous section, we start with a sub-utilization proportional to $c_v$ (and capped at $N$), and we iteratively increase the $\hat{n}_v$ that is less than $N$ and maximizes the quantity $c_v/(\hat{n}_v+1)$, until the total sum is $3N$.
\subsection{Computation of a candidate assignment}
To compute a candidate assignment (that does not optimize zone spreading nor distance to a previous assignment yet), we can use the folowing flow problem.
Define the oriented weighted graph $(X,E)$. The set of vertices $X$ contains the source $\mathbf{s}$, the sink $\mathbf{t}$, vertices
$\mathbf{x}_p, \mathbf{u}^+_p, \mathbf{u}^-_p$ for every partition $p$, vertices $\mathbf{y}_{p,z}$ for every partition $p$ and zone $z$, and vertices $\mathbf{z}_v$ for every node $v$.
The set of edges is composed of the following arcs:
\begin{itemize}
\item ($\mathbf{s}$,$\mathbf{x}_p$, 3) for every partition $p$;
\item ($\mathbf{x}_p$,$\mathbf{u}^+_p$, 3) for every partition $p$;
\item ($\mathbf{x}_p$,$\mathbf{u}^-_p$, 2) for every partition $p$;
\item ($\mathbf{u}^+_p$,$\mathbf{y}_{p,z}$, 1) for every partition $p$ and zone $z$;
\item ($\mathbf{u}^-_p$,$\mathbf{y}_{p,z}$, 2) for every partition $p$ and zone $z$;
\item ($\mathbf{y}_{p,z}$,$\mathbf{z}_v$, 1) for every partition $p$, zone $z$ and node $v\in z$;
\item ($\mathbf{z}_v$, $\mathbf{t}$, $n_v$) for every node $v$;
\end{itemize}
One can check that any maximal flow in this graph corresponds to an assignment of partitions to nodes. In such a flow, all the arcs from $\mathbf{s}$ and to $\mathbf{t}$ are saturated. The arc from $\mathbf{y}_{p,z}$ to $\mathbf{z}_v$ is saturated if and only if $p$ is associated to~$v$.
Finally the flow from $\mathbf{x}_p$ to $\mathbf{y}_{p,z}$ can go either through $\mathbf{u}^+_p$ or $\mathbf{u}^-_p$.
\subsection{Maximal spread and minimal transfers}
Notice that if the arc $\mathbf{u}_p^+\mathbf{y}_{p,z}$ is not saturated but there is some flow in $\mathbf{u}_p^-\mathbf{y}_{p,z}$, then it is possible to transfer a unit of flow from the path $\mathbf{x}_p\mathbf{u}_p^-\mathbf{y}_{p,z}$ to the path $\mathbf{x}_p\mathbf{u}_p^+\mathbf{y}_{p,z}$. So we can always find an equivalent maximal flow $f^*$ that uses the path through $\mathbf{u}_p^-$ only if the path through $\mathbf{u}_p^+$ is saturated.
We will use this fact to consider the amount of flow going through the vertices $\mathbf{u}^+$ as a measure of how well the partitions are spread over nodes belonging to different zones. If the partition $p$ is associated to 3 different zones, then a flow of 3 will cross $\mathbf{u}_p^+$ in $f^*$ (i.e. a flow of 0 will cross $\mathbf{u}_p^+$). If $p$ is associated to two zones, a flow of $2$ will cross $\mathbf{u}_p^+$. If $p$ is associated to a single zone, a flow of $1$ will cross $\mathbf{u}_p^+$.
Let $N_1, N_2, N_3$ be the number of partitions associated to respectively 1,2 and 3 distinct zones. We will optimize a linear combination of these variables using the discovery of positively weighted circuits in a graph.
At the same step, we will also optimize the distance to a previous assignment $T'$. Let $\alpha> \beta> \gamma \ge 0$ be three parameters.
Given the flow $f$, let $G_f=(X',E_f)$ be the multi-graph where $X' = X\setminus\{\mathbf{s},\mathbf{t}\}$. The set $E_f$ is composed of the arcs:
\begin{itemize}
\item As many arcs from $(\mathbf{x}_p, \mathbf{u}^+_p,\alpha), (\mathbf{x}_p, \mathbf{u}^+_p,\beta), (\mathbf{x}_p, \mathbf{u}^+_p,\gamma)$ (selected in this order) as there is flow crossing $\mathbf{u}^+_p$ in $f$;
\item As many arcs from $(\mathbf{u}^+_p, \mathbf{x}_p,-\gamma), (\mathbf{u}^+_p, \mathbf{x}_p,-\beta), (\mathbf{u}^+_p, \mathbf{x}_p,-\alpha)$ (selected in this order) as there is flow crossing $\mathbf{u}^-_p$ in $f$;
\item As many copies of $(\mathbf{x}_p, \mathbf{u}^-_p,0)$ as there is flow through $\mathbf{u}^-_p$;
\item As many copies of $(\mathbf{u}^-_p,\mathbf{x}_p,0)$ so that the number of arcs between these two vertices is 2;
\item $(\mathbf{u}^+_p,\mathbf{y}_{p,z}, 0)$ if the flow between these vertices is 1, and the opposite arc otherwise;
\item as many copies of $(\mathbf{u}^-_p,\mathbf{y}_{p,z}, 0)$ as the flow between these vertices, and as many copies of the opposite arc as 2~$-$~the flow;
\item $(\mathbf{y}_{p,z},\mathbf{z}_v, \pm1)$ if it is saturated in $f$, with $+1$ if $v\in T'_p$ and $-1$ otherwise;
\item $(\mathbf{z}_v,\mathbf{y}_{p,z}, \pm1)$ if it is not saturated in $f$, with $+1$ if $v\notin T'_p$ and $-1$ otherwise.
\end{itemize}
To summarize, arcs are oriented left to right if they correspond to a presence of flow in $f$, and right to left if they correspond to an absence of flow. They are positively weighted if we want them to stay at their current state, and negatively if we want them to switch. Let us compute the weight of such graph.
\begin{multline*}
w(G_f) = \sum_{e\in E_f} w(e_f) \\
=
(\alpha - \beta -\gamma) N_1 + (\alpha +\beta - \gamma) N_2 + (\alpha+\beta+\gamma) N_3
\\ +
\#V\times N - 4 \sum_p 3-\#(T_p\cap T'_p) \\
=(\#V-12+\alpha-\beta-\gamma)\times N + 4Q_V + 2\beta N_2 + 2(\beta+\gamma) N_3 \\
\end{multline*}
As for the mode 3-strict, one can check that the difference of two such graphs corresponding to the same $(n_v)$ is always eulerian. Hence we can navigate in this class with the same greedy algorithm that discovers positive cycles and flips them.
The function that we optimize is
$$
2Q_V + \beta N_2 + (\beta+\gamma) N_3.
$$
The choice of parameters $\beta$ and $\gamma$ should be lead by the following question: For $\beta$, where to put the tradeoff between zone dispersion and distance to the previous configuration? For $\gamma$, do we prefer to have more partitions spread between 2 zones, or have less between at least 2 zones but more between 3 zones.
The quantity $Q_V$ varies between $0$ and $3N$, it should be of order $N$. The quantity $N_2+N_3$ should also be of order $N$ (it is exactly $N$ in the strict mode). So the two terms of the function are comparable.
\bibliography{optimal_layout}
\bibliographystyle{ieeetr}
\end{document}

View file

@ -1,10 +0,0 @@
*.aux
*.bbl
*.blg
*.log
*.nav
*.out
*.snm
*.synctex.gz
*.toc
*.dvi

View file

@ -1,8 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,340 +0,0 @@
\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 saccommoder 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}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

View file

@ -1,52 +0,0 @@
\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}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

View file

@ -1,108 +0,0 @@
{
"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
}

View file

@ -1,28 +0,0 @@
{
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";
};
});
};
}

View file

@ -1,32 +1,25 @@
{ {
system, system ? builtins.currentSystem,
target ? null, target,
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 = pkgs = import pkgsSrc {
if target != null then inherit system;
import pkgsSrc { crossSystem = {
inherit system; config = target;
crossSystem = { isStatic = true;
config = target; };
isStatic = true; overlays = [ cargo2nixOverlay ];
}; };
overlays = [ cargo2nixOverlay ];
}
else
import pkgsSrc {
inherit system;
overlays = [ cargo2nixOverlay ];
};
/* /*
Cargo2nix is built for rustOverlay which installs Rust from Mozilla releases. Cargo2nix is built for rustOverlay which installs Rust from Mozilla releases.
@ -41,7 +34,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 == null || target == "x86_64-unknown-linux-musl" || target == "aarch64-unknown-linux-musl" then { if 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 {

View file

@ -11,7 +11,7 @@ PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:${NIX_RELEASE}:$PATH"
FANCYCOLORS=("41m" "42m" "44m" "45m" "100m" "104m") FANCYCOLORS=("41m" "42m" "44m" "45m" "100m" "104m")
export RUST_BACKTRACE=1 export RUST_BACKTRACE=1
export RUST_LOG=garage=info,garage_api=debug,netapp=trace export RUST_LOG=garage=info,garage_api=debug
MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m" MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m"
WHICH_GARAGE=$(which garage || exit 1) WHICH_GARAGE=$(which garage || exit 1)

View file

@ -25,7 +25,8 @@ garage -c /tmp/config.1.toml status \
| grep 'NO ROLE' \ | grep 'NO ROLE' \
| grep -Po '^[0-9a-f]+' \ | grep -Po '^[0-9a-f]+' \
| while read id; do | while read id; do
garage -c /tmp/config.1.toml layout assign $id -z dc1 -c 1 garage -c /tmp/config.1.toml layout assign $id -z dc1 -c 1G
done done
garage -c /tmp/config.1.toml layout config -r 1
garage -c /tmp/config.1.toml layout apply --version 1 garage -c /tmp/config.1.toml layout apply --version 1

View file

@ -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.2.0 version: 0.1.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

View file

@ -18,9 +18,6 @@ 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 }}
@ -83,9 +80,6 @@ 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 }}

View file

@ -85,15 +85,14 @@ service:
ingress: ingress:
s3: s3:
api: api:
enabled: false enabled: true
# 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:
@ -108,15 +107,11 @@ ingress:
# hosts: # hosts:
# - kubernetes.docker.internal # - kubernetes.docker.internal
web: web:
enabled: false enabled: true
# Rely either on the className or the annotation below but not both className: "nginx"
# 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:

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{ {
system ? builtins.currentSystem, system ? builtins.currentSystem,
}: }:
with import ./nix/common.nix; with import ./nix/common.nix;
@ -71,25 +71,13 @@ 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 -j8 \ nix copy \
--to 's3://nix?endpoint=garage.deuxfleurs.fr&region=garage&secret-key=/tmp/nix-signing-key.sec' \ --to 's3://nix?endpoint=garage.deuxfleurs.fr&region=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&region=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 \

View file

@ -1,6 +1,6 @@
[package] [package]
name = "garage_api" name = "garage_api"
version = "0.8.1" version = "0.8.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -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.1", path = "../model" } garage_model = { version = "0.8.0", path = "../model" }
garage_table = { version = "0.8.1", path = "../table" } garage_table = { version = "0.8.0", path = "../table" }
garage_block = { version = "0.8.1", path = "../block" } garage_block = { version = "0.8.0", path = "../block" }
garage_util = { version = "0.8.1", path = "../util" } garage_util = { version = "0.8.0", path = "../util" }
garage_rpc = { version = "0.8.1", path = "../rpc" } garage_rpc = { version = "0.8.0", path = "../rpc" }
async-trait = "0.1.7" async-trait = "0.1.7"
base64 = "0.13" base64 = "0.13"

View file

@ -15,7 +15,6 @@ 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::*;
@ -77,31 +76,6 @@ 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")]
{ {
@ -150,7 +124,6 @@ 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(
@ -174,10 +147,8 @@ 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,

View file

@ -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(),
unfinished_uploads: counters unfinshed_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,
unfinished_uploads: i64, unfinshed_uploads: i64,
quotas: ApiBucketQuotas, quotas: ApiBucketQuotas,
} }

View file

@ -43,11 +43,6 @@ 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>,
@ -91,7 +86,7 @@ fn get_cluster_layout(garage: &Arc<Garage>) -> GetClusterLayoutResponse {
.map(|(k, _, v)| (hex::encode(k), v.0.clone())) .map(|(k, _, v)| (hex::encode(k), v.0.clone()))
.collect(), .collect(),
staged_role_changes: layout staged_role_changes: layout
.staging .staging_roles
.items() .items()
.iter() .iter()
.filter(|(k, _, v)| layout.roles.get(k) != Some(v)) .filter(|(k, _, v)| layout.roles.get(k) != Some(v))
@ -142,14 +137,14 @@ pub async fn handle_update_cluster_layout(
let mut layout = garage.system.get_cluster_layout(); let mut layout = garage.system.get_cluster_layout();
let mut roles = layout.roles.clone(); let mut roles = layout.roles.clone();
roles.merge(&layout.staging); roles.merge(&layout.staging_roles);
for (node, role) in updates { for (node, role) in updates {
let node = hex::decode(node).ok_or_bad_request("Invalid node identifier")?; let node = hex::decode(node).ok_or_bad_request("Invalid node identifier")?;
let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?; let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?;
layout layout
.staging .staging_roles
.merge(&roles.update_mutator(node, NodeRoleV(role))); .merge(&roles.update_mutator(node, NodeRoleV(role)));
} }
@ -167,12 +162,14 @@ pub async fn handle_apply_cluster_layout(
let param = parse_json_body::<ApplyRevertLayoutRequest>(req).await?; let param = parse_json_body::<ApplyRevertLayoutRequest>(req).await?;
let layout = garage.system.get_cluster_layout(); let layout = garage.system.get_cluster_layout();
let layout = layout.apply_staged_changes(Some(param.version))?; let (layout, msg) = layout.apply_staged_changes(Some(param.version))?;
garage.system.update_cluster_layout(&layout).await?; garage.system.update_cluster_layout(&layout).await?;
Ok(Response::builder() Ok(Response::builder()
.status(StatusCode::NO_CONTENT) .status(StatusCode::OK)
.body(Body::empty())?) .header(http::header::CONTENT_TYPE, "text/plain")
.body(Body::from(msg.join("\n")))?)
} }
pub async fn handle_revert_cluster_layout( pub async fn handle_revert_cluster_layout(

View file

@ -6,7 +6,6 @@ use crate::admin::error::*;
use crate::router_macros::*; use crate::router_macros::*;
pub enum Authorization { pub enum Authorization {
None,
MetricsToken, MetricsToken,
AdminToken, AdminToken,
} }
@ -17,10 +16,8 @@ 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,
@ -91,10 +88,8 @@ 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,
@ -135,7 +130,6 @@ 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,
} }
@ -143,13 +137,9 @@ impl Endpoint {
} }
generateQueryParameters! { generateQueryParameters! {
keywords: [], "id" => id,
fields: [ "search" => search,
"format" => format, "globalAlias" => global_alias,
"id" => id, "alias" => alias,
"search" => search, "accessKeyId" => access_key_id
"globalAlias" => global_alias,
"alias" => alias,
"accessKeyId" => access_key_id
]
} }

View file

@ -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(), partition_key, query, None), (query.keyword.take().unwrap_or_default().as_ref(), 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(), partition_key, query, None), (query.keyword.take().unwrap_or_default().as_ref(), 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(), partition_key, query, None), (query.keyword.take().unwrap_or_default().as_ref(), 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(), partition_key, query, None), (query.keyword.take().unwrap_or_default().as_ref(), 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(), partition_key, query, None), (query.keyword.take().unwrap_or_default().as_ref(), 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(), partition_key, query, None), (query.keyword.take().unwrap_or_default().as_ref(), partition_key, query, None),
key: [ key: [
EMPTY => DeleteItem (query::sort_key), EMPTY => DeleteItem (query::sort_key),
], ],
@ -232,18 +232,21 @@ impl Endpoint {
// parameter name => struct field // parameter name => struct field
generateQueryParameters! { generateQueryParameters! {
keywords: [ "prefix" => prefix,
"delete" => DELETE, "start" => start,
"search" => SEARCH "causality_token" => causality_token,
], "end" => end,
fields: [ "limit" => limit,
"prefix" => prefix, "reverse" => reverse,
"start" => start, "sort_key" => sort_key,
"causality_token" => causality_token, "timeout" => timeout
"end" => end, }
"limit" => limit,
"reverse" => reverse, mod keywords {
"sort_key" => sort_key, //! This module contain all query parameters with no associated value
"timeout" => timeout //! used to differentiate endpoints.
] pub const EMPTY: &str = "";
pub const DELETE: &str = "delete";
pub const SEARCH: &str = "search";
} }

View file

@ -4,9 +4,10 @@ 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::$endpoint { .. } => true, $endpoint { .. } => true,
)* )*
_ => false _ => false
} }
@ -14,35 +15,37 @@ 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::$endpoint {$param, ..} => Some($param), $endpoint {$param, ..} => Some($param),
)* )*
_ => None _ => None
} }
}}; }};
(@gen_path_parser ($method:expr, $reqpath:expr, $query:expr) (@gen_path_parser ($method:expr, $reqpath:expr, $query:expr)
[ [
$($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),*))?,)*
]) => {{ ]) => {{
{ {
match ($method, $reqpath) { use Endpoint::*;
$( match ($method, $reqpath) {
(&Method::$meth, $path) if true $(&& $query.$required.is_some())? => Endpoint::$api { $(
$($( (&Method::$meth, $path) if true $(&& $query.$required.is_some())? => $api {
$param: router_match!(@@parse_param $query, $conv, $param), $($(
)*)? $param: router_match!(@@parse_param $query, $conv, $param),
}, )*)?
)* },
(m, p) => { )*
return Err(Error::bad_request(format!( (m, p) => {
"Unknown API endpoint: {} {}", return Err(Error::bad_request(format!(
m, p "Unknown API endpoint: {} {}",
))) m, p
} )))
} }
} }
}}; }
}};
(@gen_parser ($keyword:expr, $key:ident, $query:expr, $header:expr), (@gen_parser ($keyword:expr, $key:ident, $query:expr, $header:expr),
key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*], key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*],
no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{ no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{
@ -57,9 +60,11 @@ 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()){
$( $(
(Keyword::$kw_k, true) if true $(&& $query.$required_k.is_some())? $(&& $header.contains_key($header_k))? => Ok(Endpoint::$api_k { ($kw_k, true) if true $(&& $query.$required_k.is_some())? $(&& $header.contains_key($header_k))? => Ok($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),
@ -67,7 +72,7 @@ macro_rules! router_match {
}), }),
)* )*
$( $(
(Keyword::$kw_nk, false) $(if $query.$required_nk.is_some())? $(if $header.contains($header_nk))? => Ok(Endpoint::$api_nk { ($kw_nk, false) $(if $query.$required_nk.is_some())? $(if $header.contains($header_nk))? => Ok($api_nk {
$($( $($(
$param_nk: router_match!(@@parse_param $query, $conv_nk, $param_nk), $param_nk: router_match!(@@parse_param $query, $conv_nk, $param_nk),
)*)? )*)?
@ -79,7 +84,7 @@ macro_rules! router_match {
(@@parse_param $query:expr, query_opt, $param:ident) => {{ (@@parse_param $query:expr, query_opt, $param:ident) => {{
// extract optional query parameter // extract optional query parameter
$query.$param.take().map(|param| param.into_owned()) $query.$param.take().map(|param| param.into_owned())
}}; }};
(@@parse_param $query:expr, query, $param:ident) => {{ (@@parse_param $query:expr, query, $param:ident) => {{
// extract mendatory query parameter // extract mendatory query parameter
@ -88,7 +93,7 @@ macro_rules! router_match {
(@@parse_param $query:expr, opt_parse, $param:ident) => {{ (@@parse_param $query:expr, opt_parse, $param:ident) => {{
// extract and parse optional query parameter // extract and parse optional query parameter
// missing parameter is file, however parse error is reported as an error // missing parameter is file, however parse error is reported as an error
$query.$param $query.$param
.take() .take()
.map(|param| param.parse()) .map(|param| param.parse())
.transpose() .transpose()
@ -139,39 +144,14 @@ 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<Keyword>, keyword: Option<Cow<'a, str>>,
$( $(
$f_name: Option<Cow<'a, str>>, $name: Option<Cow<'a, str>>,
)* )*
} }
@ -180,29 +160,34 @@ 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()) {
match k.as_ref() { let repeated = match k.as_ref() {
$( $(
$kw_param => if let Some(prev_kw) = res.keyword.replace(Keyword::$kw_name) { $rest => if !v.is_empty() {
return Err(Error::bad_request(format!( res.$name.replace(v).is_some()
"Multiple keywords: '{}' and '{}'", prev_kw, $kw_param } else {
))); 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)
} }
@ -213,8 +198,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.$f_name.is_some() { else if self.$name.is_some() {
Some(concat!("'", $f_param, "'")) Some(concat!("'", $rest, "'"))
} }
)* else { )* else {
None None

View file

@ -161,15 +161,6 @@ 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!(

View file

@ -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(), key, query, None), (query.keyword.take().unwrap_or_default().as_ref(), 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(), key, query, None), (query.keyword.take().unwrap_or_default().as_ref(), 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(), key, query, None), (query.keyword.take().unwrap_or_default().as_ref(), 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(), key, query, headers), (query.keyword.take().unwrap_or_default().as_ref(), 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(), key, query, None), (query.keyword.take().unwrap_or_default().as_ref(), 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,60 +624,63 @@ impl Endpoint {
// parameter name => struct field // parameter name => struct field
generateQueryParameters! { generateQueryParameters! {
keywords: [ "continuation-token" => continuation_token,
"accelerate" => ACCELERATE, "delimiter" => delimiter,
"acl" => ACL, "encoding-type" => encoding_type,
"analytics" => ANALYTICS, "fetch-owner" => fetch_owner,
"cors" => CORS, "id" => id,
"delete" => DELETE, "key-marker" => key_marker,
"encryption" => ENCRYPTION, "list-type" => list_type,
"intelligent-tiering" => INTELLIGENT_TIERING, "marker" => marker,
"inventory" => INVENTORY, "max-keys" => max_keys,
"legal-hold" => LEGAL_HOLD, "max-parts" => max_parts,
"lifecycle" => LIFECYCLE, "max-uploads" => max_uploads,
"location" => LOCATION, "partNumber" => part_number,
"logging" => LOGGING, "part-number-marker" => part_number_marker,
"metrics" => METRICS, "prefix" => prefix,
"notification" => NOTIFICATION, "select-type" => select_type,
"object-lock" => OBJECT_LOCK, "start-after" => start_after,
"ownershipControls" => OWNERSHIP_CONTROLS, "uploadId" => upload_id,
"policy" => POLICY, "upload-id-marker" => upload_id_marker,
"policyStatus" => POLICY_STATUS, "versionId" => version_id,
"publicAccessBlock" => PUBLIC_ACCESS_BLOCK, "version-id-marker" => version_id_marker
"replication" => REPLICATION, }
"requestPayment" => REQUEST_PAYMENT,
"restore" => RESTORE, mod keywords {
"retention" => RETENTION, //! This module contain all query parameters with no associated value S3 uses to differentiate
"select" => SELECT, //! endpoints.
"tagging" => TAGGING, pub const EMPTY: &str = "";
"torrent" => TORRENT,
"uploads" => UPLOADS, pub const ACCELERATE: &str = "accelerate";
"versioning" => VERSIONING, pub const ACL: &str = "acl";
"versions" => VERSIONS, pub const ANALYTICS: &str = "analytics";
"website" => WEBSITE pub const CORS: &str = "cors";
], pub const DELETE: &str = "delete";
fields: [ pub const ENCRYPTION: &str = "encryption";
"continuation-token" => continuation_token, pub const INTELLIGENT_TIERING: &str = "intelligent-tiering";
"delimiter" => delimiter, pub const INVENTORY: &str = "inventory";
"encoding-type" => encoding_type, pub const LEGAL_HOLD: &str = "legal-hold";
"fetch-owner" => fetch_owner, pub const LIFECYCLE: &str = "lifecycle";
"id" => id, pub const LOCATION: &str = "location";
"key-marker" => key_marker, pub const LOGGING: &str = "logging";
"list-type" => list_type, pub const METRICS: &str = "metrics";
"marker" => marker, pub const NOTIFICATION: &str = "notification";
"max-keys" => max_keys, pub const OBJECT_LOCK: &str = "object-lock";
"max-parts" => max_parts, pub const OWNERSHIP_CONTROLS: &str = "ownershipControls";
"max-uploads" => max_uploads, pub const POLICY: &str = "policy";
"partNumber" => part_number, pub const POLICY_STATUS: &str = "policyStatus";
"part-number-marker" => part_number_marker, pub const PUBLIC_ACCESS_BLOCK: &str = "publicAccessBlock";
"prefix" => prefix, pub const REPLICATION: &str = "replication";
"select-type" => select_type, pub const REQUEST_PAYMENT: &str = "requestPayment";
"start-after" => start_after, pub const RESTORE: &str = "restore";
"uploadId" => upload_id, pub const RETENTION: &str = "retention";
"upload-id-marker" => upload_id_marker, pub const SELECT: &str = "select";
"versionId" => version_id, pub const TAGGING: &str = "tagging";
"version-id-marker" => version_id_marker 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)]

View file

@ -1,6 +1,6 @@
[package] [package]
name = "garage_block" name = "garage_block"
version = "0.8.1" version = "0.8.0"
authors = ["Alex Auvolat <alex@adnab.me>"] authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018" edition = "2018"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -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.1", path = "../db" } garage_db = { version = "0.8.0", path = "../db" }
garage_rpc = { version = "0.8.1", path = "../rpc" } garage_rpc = { version = "0.8.0", path = "../rpc" }
garage_util = { version = "0.8.1", path = "../util" } garage_util = { version = "0.8.0", path = "../util" }
garage_table = { version = "0.8.1", path = "../table" } garage_table = { version = "0.8.0", path = "../table" }
opentelemetry = "0.17" opentelemetry = "0.17"

Some files were not shown because too many files have changed in this diff Show more