Compare commits
620 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 582b168b6a | |||
| c821d4974a | |||
| eb1b621a5a | |||
| 0c32294485 | |||
| 21500545bb | |||
| a525d0e36a | |||
| 29e869ac88 | |||
| cd641a9ed2 | |||
| 68876f0b08 | |||
| ba1f30d393 | |||
| 9018ed9b97 | |||
| 13ded6dd35 | |||
| 4d96719d96 | |||
| 77ef3c85ea | |||
| 79c7126b67 | |||
| 77b6233496 | |||
| 547fe30a05 | |||
| bff5068efc | |||
| 60eee993b4 | |||
| d30bb2acb1 | |||
| 730c613807 | |||
| 9e356347c6 | |||
| 1d3c0511b1 | |||
| f50b342c00 | |||
| cf22e7b71d | |||
| dc8d93698b | |||
| 006e78ccea | |||
| 276e55ae8b | |||
| ab6d9633ac | |||
| cf2f058f60 | |||
| 02677af546 | |||
| fced78c283 | |||
| d211c1e291 | |||
| 318ef40c4e | |||
| c9c50b741e | |||
| a0a2c1db88 | |||
| c685a2cbaf | |||
| 04d549acc7 | |||
| a9cd3f3426 | |||
| 8f7e92b6a7 | |||
| 969f42a970 | |||
| c2d54b4136 | |||
| 424d4f8d4d | |||
| 2f7a649870 | |||
| c4836916b0 | |||
| bf5290036f | |||
| 4efc8bac07 | |||
| f3dcc39903 | |||
| b830bdd1dd | |||
| 161b185464 | |||
| 8f0d10b0b1 | |||
| 43e02920c2 | |||
| dcc2fe4ac5 | |||
| 6bac369ee2 | |||
| 96d303b05e | |||
| 3a1dce59f7 | |||
| e3a5ec6ef6 | |||
| 4d124e1c76 | |||
| 6fd2cf7966 | |||
| 6aecd9718f | |||
| d769a7be5d | |||
| 0412013229 | |||
| 511cf0c6ec | |||
| 090dbb412a | |||
| 95693d45b2 | |||
| ca296477f3 | |||
| ca3b4a050d | |||
| a057ab23ea | |||
| 12367d307b | |||
| 675c1c156d | |||
| eac3a60050 | |||
| 58bc65b9a8 | |||
| a5580c99fe | |||
| 8083bb4a0f | |||
| 00cdefa6b3 | |||
| 4dee3e6e04 | |||
| 5333285b50 | |||
| ef913843f7 | |||
| ac851d6dee | |||
| eac2aa6fe4 | |||
| 1e0201ada2 | |||
| 1fe932d07f | |||
| 82297371bf | |||
| 7e5bb51287 | |||
| 00a5c3d8a2 | |||
| 174f4f01a8 | |||
| 1aac7b4875 | |||
| b43c58cbe5 | |||
| 9481ac428e | |||
| 1c29d04cc5 | |||
| b48a8eaa1f | |||
| 42fd8583bd | |||
| 2e7a6fccba | |||
| 09041035d5 | |||
| ae64ecf10c | |||
| c8ac4a2105 | |||
| 280c1303fa | |||
| 0f5b3878ca | |||
| d863247f9f | |||
| 6c740ff05c | |||
| 95d9905524 | |||
| 53fe77860b | |||
| 6d5e971974 | |||
| 4d8407dc0f | |||
| 006fb18aea | |||
| b43f309ec7 | |||
| df4721387c | |||
| 236af3a958 | |||
| 4b1fdbef55 | |||
| 0f1b488be0 | |||
| 9c067c0cbd | |||
| 0bbf63ee0e | |||
| 879d941d7b | |||
| d726cf0299 | |||
| 0c7aeab6f8 | |||
| 5687fc0375 | |||
| 742129f4a3 | |||
| 97f1e9ab52 | |||
| 60b1d78b56 | |||
| 4c895a7186 | |||
| 7a256b2ebb | |||
| 909359ca4c | |||
| 3148fa3afe | |||
| 6b06459b99 | |||
| 4c139bcbca | |||
| c3b5cbf212 | |||
| 57a467b5c0 | |||
| 6cf6db5c61 | |||
| d5a57e3e13 | |||
| 5cf354acb4 | |||
| 4758d8881f | |||
| 61e19310c8 | |||
| 2b007ddea3 | |||
| c8599a8636 | |||
| 17fe11fa81 | |||
| 16128fca63 | |||
| 29570f3192 | |||
| c35c1b5b9b | |||
| 0b901bf291 | |||
| 7e203f634e | |||
| 99f7c0fc4b | |||
| c8c20d6f47 | |||
| fb95a8819f | |||
| 665addc03b | |||
| 7949927291 | |||
| 2ddb29ca35 | |||
| e5db610e4c | |||
| 65c6f8adea | |||
| 54b9bf02a3 | |||
| 469153233f | |||
| 90bba5889a | |||
| a64b567d43 | |||
| 6ea86db8cd | |||
| aa69c06f2b | |||
| a6c6c44310 | |||
| 96d7713915 | |||
| 30d8ec5368 | |||
| 47772eb525 | |||
| d64498c3d3 | |||
| c1ed770e64 | |||
| 7e80e86934 | |||
| 4deb57815a | |||
| df343dd808 | |||
| 17c73bafa2 | |||
| d8058e7475 | |||
| 385fbc606d | |||
| 6f9d6919a9 | |||
| b340599e68 | |||
| 91fde4105d | |||
| d975960be3 | |||
| 5448012b27 | |||
| ce34d11a65 | |||
| 8cb7623ebd | |||
| 5469c95877 | |||
| f930c6f643 | |||
| 6508acbe71 | |||
| afcb22bf16 | |||
| cc29a40d51 | |||
| 0f3f180c3e | |||
| 70cf6004ae | |||
| 985ad68ade | |||
| c7571ff89b | |||
| b7a853b01f | |||
| 66faef9fb6 | |||
| 13f67b6cd8 | |||
| 0dabf9b22f | |||
| 1b42919bf7 | |||
| e226fb413f | |||
| 708a84f1d6 | |||
| 0465475599 | |||
| 0a45317b3b | |||
| bb3b832024 | |||
| f8be15c37d | |||
| 1e05fc1d53 | |||
| e5eff872f5 | |||
| 605ee4cdb1 | |||
| 71aef8770e | |||
| b4f6ab963c | |||
| 9a31b9c077 | |||
| 58a96dc687 | |||
| 7bbb3ff9cf | |||
| f04af18193 | |||
| 67e0fcc6ea | |||
| 78f03aec78 | |||
| 56a23d936e | |||
| 9b6e45ca1f | |||
| 27666ed265 | |||
| e8e722cc66 | |||
| 80f818eb6c | |||
| f899e023a0 | |||
| 7556c536ae | |||
| 2a20319fa9 | |||
| 42baa29e50 | |||
| f461348790 | |||
| 4a8f7e15ce | |||
| 44587d295a | |||
| 3f4ab3a4a3 | |||
| 3a4afc04a9 | |||
| fbf03e9378 | |||
| dc1a4ffd76 | |||
| 9eb07d4c7b | |||
| 85ee4f5d8c | |||
| 328072d122 | |||
| 26bc807905 | |||
| a9f5f242b2 | |||
| ae98abca5c | |||
| adfa44ad70 | |||
| 47143b88ad | |||
| 8843aa92fa | |||
| b601b3e46d | |||
| a19d2f16e2 | |||
| fc8fc60f6d | |||
| 77079a1498 | |||
| 2a4f729b57 | |||
| 1b042e379e | |||
| 53005c91a5 | |||
| ffbce0f689 | |||
| b7a153b892 | |||
| bc8e6af223 | |||
| 37e5621dde | |||
| 6529ff379a | |||
| 78b1481461 | |||
| 7ab1d176d4 | |||
| a8d73682a4 | |||
| 8654eb19bf | |||
| 54ea412188 | |||
| 2ade8c86f6 | |||
| b15e2cbb6c | |||
| 0fd1b7342b | |||
| b15d55ea9f | |||
| c13af97b81 | |||
| d1d5c67ba7 | |||
| 77125e9464 | |||
| cfd10480ee | |||
| fbb40c4ea0 | |||
| e475c7f802 | |||
| 589a992af8 | |||
| 768794daae | |||
| abe0546ab0 | |||
| be16bc7a05 | |||
| bfaa1ca6b7 | |||
| de8eeab4ad | |||
| ae3f7ee76c | |||
| 2dc3a6dbbe | |||
| c6bc3f229b | |||
| 47fe96279b | |||
| bba9202f31 | |||
| a605a80806 | |||
| 539af12d21 | |||
| 45bdf54e7e | |||
| a4b431163c | |||
| db54bf96c7 | |||
| a2a9e3cec4 | |||
| 14274bc13c | |||
| cbcdab4e24 | |||
| 38ca35eb0f | |||
| a2d87a012d | |||
| 899292ee28 | |||
| bf4691d98a | |||
| ad151cb1dc | |||
| c8e9c45889 | |||
| 3c20984a08 | |||
| e6e4e051a1 | |||
| 9b38cba6f3 | |||
| 4ef954d176 | |||
| 02498a93d0 | |||
| 4caad5425d | |||
| e79b485aa8 | |||
| d38d62f4d7 | |||
| 2885806e00 | |||
| 52437e4298 | |||
| abcef7a3fd | |||
| 5d338f0b8f | |||
| 590c9bb4db | |||
| c56b7e20c3 | |||
| 2f21181ccb | |||
| 9ec3f8cc3c | |||
| 2d1c073d2f | |||
| 5e7307cbf3 | |||
| fd0e23e984 | |||
| d7506b282c | |||
| 6bbdca2e48 | |||
| c6d6cc1fc3 | |||
| 5fa6df6ee3 | |||
| c6bed26347 | |||
| d25e631a4a | |||
| 514eb29874 | |||
| 8ba6454e21 | |||
| 9dcc5232a6 | |||
| 1e13a66b42 | |||
| 2c9e849bbf | |||
| 34baade499 | |||
| 2f2a96b51d | |||
| c9156f6828 | |||
| 14d2f2b18d | |||
| a7d845a999 | |||
| dd20e5d22a | |||
| 4629ee25f7 | |||
| a826c361a9 | |||
| fb6db494cc | |||
| 97e2fa5b8b | |||
| cfd259190f | |||
| 6906a4ff12 | |||
| 9053782d71 | |||
| 48e0436f29 | |||
| 9c745548c4 | |||
| f7d9c2b383 | |||
| e6862c5d3d | |||
| d032e2017c | |||
| 0b12debf6c | |||
| 795b4a41b7 | |||
| fd2472d488 | |||
| d2a064bb1b | |||
| 88b4623bf1 | |||
| 325f79012c | |||
| eb40475f1e | |||
| 22c0420607 | |||
| 1bd7689301 | |||
| ec0da3b644 | |||
| 9511b20153 | |||
| d067a40b3f | |||
| ff6ec62d54 | |||
| 004eb94e14 | |||
| 46f620119b | |||
| 576d0d950e | |||
| 85a07c87d7 | |||
| 1f645830a4 | |||
| 5f308bd688 | |||
| df758e8e0d | |||
| e83864af24 | |||
| 3b49dd9e63 | |||
| cef8d75983 | |||
| c96be1a9a8 | |||
| 98e56490a1 | |||
| e791ccec8f | |||
| d605c4fed1 | |||
| cd0728cd20 | |||
| 0951b5db75 | |||
| 3d94eb8d4b | |||
| 004866caac | |||
| 913e6da41b | |||
| e4881e62f1 | |||
| 7ccbfda26d | |||
| 6b19d7628e | |||
| 411f1d495c | |||
| ba68506c36 | |||
| 21c83ab311 | |||
| 2e03d90585 | |||
| 0ce5f7eb00 | |||
| 29ce490dd6 | |||
| 516255321f | |||
| f3b05ff771 | |||
| e254cc20e5 | |||
| 12f15c4c2b | |||
| 42c5d02cdf | |||
| 4689b10448 | |||
| 156b10ee65 | |||
| 8647ebf003 | |||
| 67d7c0769b | |||
| 09ed5ab8cc | |||
| a0ea28b0da | |||
| c5237c31e7 | |||
| f87943a39d | |||
| c0846c56fe | |||
| 1cb0ae10a8 | |||
| 1a8f74fc94 | |||
| 2191620af5 | |||
| bf27a3ec98 | |||
| f64ec6e542 | |||
| 6d38907dac | |||
| cfe8e8d45c | |||
| f6e805e7db | |||
| 45e10e55f9 | |||
| 730bfee753 | |||
| ccab0e4ae5 | |||
| abb60dcf7e | |||
| f8b0817ddc | |||
| 21c0dda16a | |||
| 658541d812 | |||
| c5df820e2c | |||
| a04d6cd5b8 | |||
| 44a896f9b5 | |||
| cee7560fc1 | |||
| 2f0c5ca220 | |||
| 859b38b0d2 | |||
| 2729a71d9d | |||
| c9d00f5f7b | |||
| 89c944ebd6 | |||
| 24470377c9 | |||
| 5b26545abf | |||
| 9c7e3c7bde | |||
| 165f9316e2 | |||
| a94adf804f | |||
| e4c9a8cd53 | |||
| 9312c6bbcb | |||
| fdf4dad728 | |||
| 6820b69f30 | |||
| d0104b9f9b | |||
| 3fe8db9e52 | |||
| 627a37fe9f | |||
| 2f55889835 | |||
| 8b9cc5ca3f | |||
| a1533d2919 | |||
| c1b39d9ba1 | |||
| d84308c413 | |||
| c3e8e5e38c | |||
| 62a3003cca | |||
| 3151695011 | |||
| 63f20bdeab | |||
| f034e834fa | |||
| bf0f792418 | |||
| a2e134f036 | |||
| 06aa4b604f | |||
| 61f3de6496 | |||
| 71655c1e89 | |||
| d3226bfa91 | |||
| af67626ab2 | |||
| 5475da8ea8 | |||
| 620dc58560 | |||
| 7c8fc04b96 | |||
| f914db057a | |||
| 406b6da163 | |||
| 9f468b4439 | |||
| 97be7b38fa | |||
| 6a1079c412 | |||
| b1629dd355 | |||
| d405a9f839 | |||
| 7b9c047b11 | |||
| 10bbb26b30 | |||
| 89ff9f5576 | |||
| bdaf55ab3f | |||
| e96014ca60 | |||
| 47e87c8739 | |||
| 34599bff51 | |||
| ec1a475923 | |||
| b9df2d1ad1 | |||
| 390a5d97fe | |||
| 568c4954e9 | |||
| 4dc2bc337f | |||
| 5dd2791981 | |||
| fe937c2901 | |||
| d601f31186 | |||
| e4de7bdfd5 | |||
| d18c5ad0ff | |||
| 3d5e9a027e | |||
| f4ca7758b4 | |||
| 4563313f87 | |||
| afa28706e5 | |||
| 84f1db91c4 | |||
| 9fa20d45be | |||
| 9330fd79d3 | |||
| 83f6928ff7 | |||
| 3192088aac | |||
| 5a89350b38 | |||
| 3caea5fc06 | |||
| ebc0e9319e | |||
| f8c6a8373d | |||
| 076ce04fe5 | |||
| f37d5d2b08 | |||
| 819f4f0050 | |||
| 69ddaafc60 | |||
| 145130481e | |||
| 6ed78abb5c | |||
| 19454c1679 | |||
| 1c03941b19 | |||
| 4f0b923c4f | |||
| 420bbc162d | |||
| 12ea4cda5f | |||
| 5fefbd94e9 | |||
| ba810b2e81 | |||
| f8ed3fdbc4 | |||
| 2daeb89834 | |||
| 4cb45bd398 | |||
| d5ad797ad7 | |||
| a99925e0ed | |||
| f538dc34d3 | |||
| ed58f8b0fe | |||
| 5037b97dd4 | |||
| af1a530834 | |||
| c99bfe69ea | |||
| 831f2b0207 | |||
| c1eb1610ba | |||
| 5560a963e0 | |||
| ab71544499 | |||
| 991edbe02c | |||
| 9f3c7c3720 | |||
| bfde9152b8 | |||
| 7bb042f0b7 | |||
| a1d081ee84 | |||
| e8fa89e834 | |||
| beedc9fd11 | |||
| 6d798c640f | |||
| d4e3e60920 | |||
| 43402c9619 | |||
| efa6f3d85e | |||
| 74a1b49b13 | |||
| 23d57b89dc | |||
| 5e3e1f4453 | |||
| 59c153d280 | |||
| bb3e0f7d22 | |||
| 0156e40c9d | |||
| f6f88065ad | |||
| 591bd808ec | |||
| 294cb99409 | |||
| 2eb9fcae20 | |||
| 58b9eb46fc | |||
| 255b01b626 | |||
| 58a765c51f | |||
| 1c431b8457 | |||
| 39ac034de5 | |||
| 8ddb0dd485 | |||
| 83887a8519 | |||
| 0a15db6960 | |||
| 295237476e | |||
| 9d83605736 | |||
| 4b1a7fb5e3 | |||
| b6aaebaf4c | |||
| 2aaba39ddc | |||
| 47467df83e | |||
| 9b7fea4cb0 | |||
| 44ce6ae5b4 | |||
| 22487ceddf | |||
| 7bbc8fec50 | |||
| 6ccfbb2986 | |||
| 6689800986 | |||
| d2246baab7 | |||
| afac1d4d4a | |||
| 6ca99fd02c | |||
| b568bb863d | |||
| b8f301a61d | |||
| 428ad2075d | |||
| c939d2a936 | |||
| 65e9dde8c9 | |||
| c9b733a4a6 | |||
| 3661a597fa | |||
| 0fd3c0e794 | |||
| 4c1bf42192 | |||
| 906c8708fd | |||
| 747889a096 | |||
| feb09a4bc6 | |||
| aa8bc6aa88 | |||
| aba7902995 | |||
| 78de7b5bde | |||
| 9bd9e392ba | |||
| 116ad479a8 | |||
| b6a58c5c16 | |||
| 2b0bfa9b18 | |||
| a18b3f0d1f | |||
| 7a143f46fc | |||
| c731f0291a | |||
| 34453bc9c2 | |||
| 6da1353541 | |||
| bd71728874 | |||
| 51ced60366 | |||
| 586957b4b7 | |||
| 8d2bb4afeb | |||
| c26f32b769 | |||
| 8062ec7b4b | |||
| eb416a02fb | |||
| 74363c9060 | |||
| 615698df7d | |||
| 7061fa5a56 | |||
| 8881930cdd | |||
| 52f6c0760b | |||
| 5b0602c7e9 | |||
| 182b2af7e5 | |||
| baf32c9575 | |||
| 3dda1ee4f6 | |||
| aa7ce9e97c | |||
| 8d62616ec0 | |||
| bd6fe72c06 | |||
| 4c9e8ef625 | |||
| 3e711bc110 | |||
| 7fb66b4944 | |||
| 679ae8bcbb | |||
| 2a93ad0c84 | |||
| f190032589 | |||
| 3a87bd1370 | |||
| 9302cd42f0 | |||
| 060ad0da32 | |||
| a5ed1161c6 | |||
| 222674432b | |||
| 070a8ad110 | |||
| 770384cae1 | |||
| a0f6bc5b7f | |||
| 88c734bbd9 | |||
| d38509ef4b | |||
| 39b37833c5 | |||
| a2c1de646b | |||
| 15847a636a | |||
| 123d3e1f04 | |||
| a6e4b96ca9 | |||
| b442b0e35e | |||
| 0c3b198b22 | |||
| 33c2086d9e | |||
| 5ad1e55ccf | |||
| 1779fd40c0 | |||
| b47706809c | |||
| 126e0f47a3 | |||
| 8b35a946d9 |
@@ -1,3 +0,0 @@
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
@@ -1,3 +1,6 @@
|
||||
labels:
|
||||
nix: "enabled"
|
||||
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
@@ -9,39 +12,33 @@ when:
|
||||
|
||||
steps:
|
||||
- name: check formatting
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-shell --attr devShell --run "cargo fmt -- --check"
|
||||
- nix-build -j4 --attr flakePackages.fmt
|
||||
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
|
||||
- nix-build -j4 --attr flakePackages.dev
|
||||
|
||||
- name: unit + func tests
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
environment:
|
||||
GARAGE_TEST_INTEGRATION_EXE: result-bin/bin/garage
|
||||
GARAGE_TEST_INTEGRATION_PATH: tmp-garage-integration
|
||||
- name: unit + func tests (lmdb)
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
|
||||
- nix-build --no-build-output --attr test.amd64
|
||||
- ./result/bin/garage_db-*
|
||||
- ./result/bin/garage_api-*
|
||||
- ./result/bin/garage_model-*
|
||||
- ./result/bin/garage_rpc-*
|
||||
- ./result/bin/garage_table-*
|
||||
- ./result/bin/garage_util-*
|
||||
- ./result/bin/garage_web-*
|
||||
- ./result/bin/garage-*
|
||||
- GARAGE_TEST_INTEGRATION_DB_ENGINE=lmdb ./result/bin/integration-* || (cat tmp-garage-integration/stderr.log; false)
|
||||
- nix-shell --attr ci --run "killall -9 garage" || true
|
||||
- GARAGE_TEST_INTEGRATION_DB_ENGINE=sqlite ./result/bin/integration-* || (cat tmp-garage-integration/stderr.log; false)
|
||||
- rm result
|
||||
- rm -rv tmp-garage-integration
|
||||
- nix-build -j4 --attr flakePackages.tests-lmdb
|
||||
|
||||
- name: unit + func tests (sqlite)
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build -j4 --attr flakePackages.tests-sqlite
|
||||
|
||||
- name: unit + func tests (fjall)
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build -j4 --attr flakePackages.tests-fjall
|
||||
|
||||
- name: integration tests
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
|
||||
- nix-build -j4 --attr flakePackages.dev
|
||||
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
depends_on: [ build ]
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
labels:
|
||||
nix: "enabled"
|
||||
|
||||
when:
|
||||
event:
|
||||
- deployment
|
||||
@@ -8,20 +11,21 @@ depends_on:
|
||||
|
||||
steps:
|
||||
- name: refresh-index
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
secrets:
|
||||
- source: garagehq_aws_access_key_id
|
||||
target: AWS_ACCESS_KEY_ID
|
||||
- source: garagehq_aws_secret_access_key
|
||||
target: AWS_SECRET_ACCESS_KEY
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: garagehq_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: garagehq_aws_secret_access_key
|
||||
commands:
|
||||
- mkdir -p /etc/nix && cp nix/nix.conf /etc/nix/nix.conf
|
||||
- nix-shell --attr ci --run "refresh_index"
|
||||
|
||||
- name: multiarch-docker
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
secrets:
|
||||
- docker_auth
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
environment:
|
||||
DOCKER_AUTH:
|
||||
from_secret: docker_auth
|
||||
commands:
|
||||
- mkdir -p /root/.docker
|
||||
- echo $DOCKER_AUTH > /root/.docker/config.json
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
labels:
|
||||
nix: "enabled"
|
||||
|
||||
when:
|
||||
event:
|
||||
- deployment
|
||||
@@ -16,18 +19,17 @@ matrix:
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build --no-build-output --attr pkgs.${ARCH}.release --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
|
||||
- nix-build --attr releasePackages.${ARCH} --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
|
||||
|
||||
- name: check is static binary
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-build --no-build-output --attr pkgs.${ARCH}.release --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
|
||||
- nix-shell --attr ci --run "./script/not-dynamic.sh result-bin/bin/garage"
|
||||
- nix-shell --attr ci --run "./script/not-dynamic.sh result/bin/garage"
|
||||
|
||||
- name: integration tests
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
when:
|
||||
@@ -36,8 +38,16 @@ steps:
|
||||
- matrix:
|
||||
ARCH: i386
|
||||
|
||||
- name: upgrade tests
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
- name: upgrade tests from v1.0.0
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-shell --attr ci --run "./script/test-upgrade.sh v1.0.0 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
|
||||
when:
|
||||
- matrix:
|
||||
ARCH: amd64
|
||||
|
||||
- name: upgrade tests from v0.8.4
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
commands:
|
||||
- nix-shell --attr ci --run "./script/test-upgrade.sh v0.8.4 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
|
||||
when:
|
||||
@@ -45,24 +55,23 @@ steps:
|
||||
ARCH: amd64
|
||||
|
||||
- name: push static binary
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
environment:
|
||||
TARGET: "${TARGET}"
|
||||
secrets:
|
||||
- source: garagehq_aws_access_key_id
|
||||
target: AWS_ACCESS_KEY_ID
|
||||
- source: garagehq_aws_secret_access_key
|
||||
target: AWS_SECRET_ACCESS_KEY
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: garagehq_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: garagehq_aws_secret_access_key
|
||||
commands:
|
||||
- nix-shell --attr ci --run "to_s3"
|
||||
|
||||
- name: docker build and publish
|
||||
image: nixpkgs/nix:nixos-22.05
|
||||
image: nixpkgs/nix:nixos-24.05
|
||||
environment:
|
||||
DOCKER_PLATFORM: "linux/${ARCH}"
|
||||
CONTAINER_NAME: "dxflrs/${ARCH}_garage"
|
||||
secrets:
|
||||
- docker_auth
|
||||
DOCKER_AUTH:
|
||||
from_secret: docker_auth
|
||||
commands:
|
||||
- mkdir -p /root/.docker
|
||||
- echo $DOCKER_AUTH > /root/.docker/config.json
|
||||
|
||||
@@ -8,7 +8,10 @@ members = [
|
||||
"src/table",
|
||||
"src/block",
|
||||
"src/model",
|
||||
"src/api",
|
||||
"src/api/common",
|
||||
"src/api/s3",
|
||||
"src/api/k2v",
|
||||
"src/api/admin",
|
||||
"src/web",
|
||||
"src/garage",
|
||||
"src/k2v-client",
|
||||
@@ -21,15 +24,18 @@ default-members = ["src/garage"]
|
||||
|
||||
# Internal Garage crates
|
||||
format_table = { version = "0.1.1", path = "src/format-table" }
|
||||
garage_api = { version = "1.0.0", path = "src/api" }
|
||||
garage_block = { version = "1.0.0", path = "src/block" }
|
||||
garage_db = { version = "1.0.0", path = "src/db", default-features = false }
|
||||
garage_model = { version = "1.0.0", path = "src/model", default-features = false }
|
||||
garage_net = { version = "1.0.0", path = "src/net" }
|
||||
garage_rpc = { version = "1.0.0", path = "src/rpc" }
|
||||
garage_table = { version = "1.0.0", path = "src/table" }
|
||||
garage_util = { version = "1.0.0", path = "src/util" }
|
||||
garage_web = { version = "1.0.0", path = "src/web" }
|
||||
garage_api_common = { version = "2.2.0", path = "src/api/common" }
|
||||
garage_api_admin = { version = "2.2.0", path = "src/api/admin" }
|
||||
garage_api_s3 = { version = "2.2.0", path = "src/api/s3" }
|
||||
garage_api_k2v = { version = "2.2.0", path = "src/api/k2v" }
|
||||
garage_block = { version = "2.2.0", path = "src/block" }
|
||||
garage_db = { version = "2.2.0", path = "src/db", default-features = false }
|
||||
garage_model = { version = "2.2.0", path = "src/model", default-features = false }
|
||||
garage_net = { version = "2.2.0", path = "src/net" }
|
||||
garage_rpc = { version = "2.2.0", path = "src/rpc" }
|
||||
garage_table = { version = "2.2.0", path = "src/table" }
|
||||
garage_util = { version = "2.2.0", path = "src/util" }
|
||||
garage_web = { version = "2.2.0", path = "src/web" }
|
||||
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
|
||||
|
||||
# External crates from crates.io
|
||||
@@ -42,25 +48,24 @@ blake2 = "0.10"
|
||||
bytes = "1.0"
|
||||
bytesize = "1.1"
|
||||
cfg-if = "1.0"
|
||||
chrono = "0.4"
|
||||
crc32fast = "1.4"
|
||||
crc32c = "0.6"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
crc-fast = "1.6"
|
||||
crypto-common = "0.1"
|
||||
digest = "0.10"
|
||||
err-derive = "0.3"
|
||||
gethostname = "0.4"
|
||||
git-version = "0.3.4"
|
||||
hex = "0.4"
|
||||
hexdump = "0.1"
|
||||
hmac = "0.12"
|
||||
idna = "0.5"
|
||||
itertools = "0.12"
|
||||
ipnet = "2.9.0"
|
||||
lazy_static = "1.4"
|
||||
md-5 = "0.10"
|
||||
mktemp = "0.5"
|
||||
nix = { version = "0.27", default-features = false, features = ["fs"] }
|
||||
nix = { version = "0.29", default-features = false, features = ["fs"] }
|
||||
nom = "7.1"
|
||||
parking_lot = "0.12"
|
||||
parse_duration = "2.1"
|
||||
paste = "1.0"
|
||||
pin-project = "1.0.12"
|
||||
pnet_datalink = "0.34"
|
||||
rand = "0.8"
|
||||
@@ -78,12 +83,14 @@ pretty_env_logger = "0.5"
|
||||
structopt = { version = "0.3", default-features = false }
|
||||
syslog-tracing = "0.3"
|
||||
tracing = "0.1"
|
||||
tracing-journald = "0.3.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
heed = { version = "0.11", default-features = false, features = ["lmdb"] }
|
||||
rusqlite = "0.31.0"
|
||||
rusqlite = "0.37"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.24"
|
||||
r2d2_sqlite = "0.31"
|
||||
fjall = "2.4"
|
||||
|
||||
async-compression = { version = "0.4", features = ["tokio", "zstd"] }
|
||||
zstd = { version = "0.13", default-features = false }
|
||||
@@ -94,6 +101,7 @@ serde = { version = "1.0", default-features = false, features = ["derive", "rc"]
|
||||
serde_bytes = "0.11"
|
||||
serde_json = "1.0"
|
||||
toml = { version = "0.8", default-features = false, features = ["parse"] }
|
||||
utoipa = { version = "5.3.1", features = ["chrono"] }
|
||||
|
||||
# newer version requires rust edition 2021
|
||||
k8s-openapi = { version = "0.21", features = ["v1_24"] }
|
||||
@@ -126,19 +134,18 @@ opentelemetry-contrib = "0.9"
|
||||
prometheus = "0.13"
|
||||
|
||||
# used by the k2v-client crate only
|
||||
aws-sigv4 = { version = "1.1" }
|
||||
hyper-rustls = { version = "0.26", features = ["http2"] }
|
||||
aws-sigv4 = { version = "1.1", default-features = false }
|
||||
hyper-rustls = { version = "0.26", default-features = false, features = ["http1", "http2", "ring", "rustls-native-certs"] }
|
||||
log = "0.4"
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0"
|
||||
|
||||
# ---- used only as build / dev dependencies ----
|
||||
assert-json-diff = "2.0"
|
||||
rustc_version = "0.4.0"
|
||||
static_init = "1.0"
|
||||
|
||||
aws-config = "1.1.4"
|
||||
aws-sdk-config = "1.13"
|
||||
aws-sdk-s3 = "1.14"
|
||||
aws-smithy-runtime = { version = "1.8", default-features = false, features = ["tls-rustls"] }
|
||||
aws-sdk-config = { version = "1.62", default-features = false }
|
||||
aws-sdk-s3 = { version = "1.79", default-features = false, features = ["rt-tokio"] }
|
||||
|
||||
[profile.dev]
|
||||
#lto = "thin" # disabled for now, adds 2-4 min to each CI build
|
||||
@@ -147,5 +154,5 @@ lto = "off"
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = "s"
|
||||
opt-level = 3
|
||||
strip = true
|
||||
|
||||
@@ -3,5 +3,5 @@ FROM scratch
|
||||
ENV RUST_BACKTRACE=1
|
||||
ENV RUST_LOG=garage=info
|
||||
|
||||
COPY result-bin/bin/garage /
|
||||
COPY result/bin/garage /
|
||||
CMD [ "/garage", "server"]
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
.PHONY: doc all release shell run1 run2 run3
|
||||
.PHONY: doc all run1 run2 run3
|
||||
|
||||
all:
|
||||
clear; cargo build
|
||||
|
||||
release:
|
||||
nix-build --attr pkgs.amd64.release --no-build-output
|
||||
|
||||
shell:
|
||||
nix-shell
|
||||
clear
|
||||
cargo build
|
||||
|
||||
# ----
|
||||
|
||||
|
||||
@@ -3,53 +3,22 @@
|
||||
with import ./nix/common.nix;
|
||||
|
||||
let
|
||||
pkgs = import pkgsSrc { };
|
||||
pkgs = import nixpkgs { };
|
||||
compile = import ./nix/compile.nix;
|
||||
|
||||
build_debug_and_release = (target: {
|
||||
debug = (compile {
|
||||
inherit system target git_version pkgsSrc cargo2nixOverlay;
|
||||
release = false;
|
||||
}).workspace.garage { compileMode = "build"; };
|
||||
|
||||
release = (compile {
|
||||
inherit system target git_version pkgsSrc cargo2nixOverlay;
|
||||
release = true;
|
||||
}).workspace.garage { compileMode = "build"; };
|
||||
});
|
||||
|
||||
test = (rustPkgs:
|
||||
pkgs.symlinkJoin {
|
||||
name = "garage-tests";
|
||||
paths =
|
||||
builtins.map (key: rustPkgs.workspace.${key} { compileMode = "test"; })
|
||||
(builtins.attrNames rustPkgs.workspace);
|
||||
});
|
||||
build_release = target: (compile {
|
||||
inherit target system git_version nixpkgs;
|
||||
crane = flake.inputs.crane;
|
||||
rust-overlay = flake.inputs.rust-overlay;
|
||||
release = true;
|
||||
}).garage;
|
||||
|
||||
in {
|
||||
pkgs = {
|
||||
amd64 = build_debug_and_release "x86_64-unknown-linux-musl";
|
||||
i386 = build_debug_and_release "i686-unknown-linux-musl";
|
||||
arm64 = build_debug_and_release "aarch64-unknown-linux-musl";
|
||||
arm = build_debug_and_release "armv6l-unknown-linux-musleabihf";
|
||||
};
|
||||
test = {
|
||||
amd64 = test (compile {
|
||||
inherit system git_version pkgsSrc cargo2nixOverlay;
|
||||
target = "x86_64-unknown-linux-musl";
|
||||
features = [
|
||||
"garage/bundled-libs"
|
||||
"garage/k2v"
|
||||
"garage/lmdb"
|
||||
"garage/sqlite"
|
||||
];
|
||||
});
|
||||
};
|
||||
clippy = {
|
||||
amd64 = (compile {
|
||||
inherit system git_version pkgsSrc cargo2nixOverlay;
|
||||
target = "x86_64-unknown-linux-musl";
|
||||
compiler = "clippy";
|
||||
}).workspace.garage { compileMode = "build"; };
|
||||
releasePackages = {
|
||||
amd64 = build_release "x86_64-unknown-linux-musl";
|
||||
i386 = build_release "i686-unknown-linux-musl";
|
||||
arm64 = build_release "aarch64-unknown-linux-musl";
|
||||
arm = build_release "armv6l-unknown-linux-musleabihf";
|
||||
};
|
||||
flakePackages = flake.packages.${system};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Garage Adminstration API v0</title>
|
||||
<title>Garage administration API v0</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Garage Adminstration API v0</title>
|
||||
<title>Garage administration API v1</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
@@ -687,7 +687,7 @@ paths:
|
||||
operationId: "GetBucketInfo"
|
||||
summary: "Get a bucket"
|
||||
description: |
|
||||
Given a bucket identifier (`id`) or a global alias (`alias`), get its information.
|
||||
Given a bucket identifier (`id`) or a global alias (`globalAlias`), get its information.
|
||||
It includes its aliases, its web configuration, keys that have some permissions
|
||||
on it, some statistics (number of objects, size), number of dangling multipart uploads,
|
||||
and its quotas (if any).
|
||||
@@ -701,7 +701,7 @@ paths:
|
||||
example: "b4018dc61b27ccb5c64ec1b24f53454bbbd180697c758c4d47a22a8921864a87"
|
||||
schema:
|
||||
type: string
|
||||
- name: alias
|
||||
- name: globalAlias
|
||||
in: query
|
||||
description: |
|
||||
The exact global alias of one of the existing buckets.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Garage administration API v2</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="./css/redoc.css" rel="stylesheet">
|
||||
|
||||
<!--
|
||||
Redoc doesn't change outer page styles
|
||||
-->
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url='./garage-admin-v2.json'></redoc>
|
||||
<script src="./redoc.standalone.js"> </script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,7 +23,7 @@ client = minio.Minio(
|
||||
"GKyourapikey",
|
||||
"abcd[...]1234",
|
||||
# Force the region, this is specific to garage
|
||||
region="region",
|
||||
region="garage",
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ In this section, we cover the following web applications:
|
||||
| [Mastodon](#mastodon) | ✅ | Natively supported |
|
||||
| [Matrix](#matrix) | ✅ | Tested with `synapse-s3-storage-provider` |
|
||||
| [ejabberd](#ejabberd) | ✅ | `mod_s3_upload` |
|
||||
| [Pixelfed](#pixelfed) | ❓ | Not yet tested |
|
||||
| [Pleroma](#pleroma) | ❓ | Not yet tested |
|
||||
| [Ente](#ente) | ✅ | Natively supported |
|
||||
| [Pixelfed](#pixelfed) | ❓ | Natively supported |
|
||||
| [Pleroma](#pleroma) | ✅ | Natively supported |
|
||||
| [Lemmy](#lemmy) | ✅ | Supported with pict-rs |
|
||||
| [Funkwhale](#funkwhale) | ❓ | Not yet tested |
|
||||
| [Misskey](#misskey) | ❓ | Not yet tested |
|
||||
@@ -69,7 +70,7 @@ $CONFIG = array(
|
||||
'hostname' => '127.0.0.1', // Can also be a domain name, eg. garage.example.com
|
||||
'port' => 3900, // Put your reverse proxy port or your S3 API port
|
||||
'use_ssl' => false, // Set it to true if you have a TLS enabled reverse proxy
|
||||
'region' => 'garage', // Garage has only one region named "garage"
|
||||
'region' => 'garage', // Garage default region is named "garage", edit according to your cluster config
|
||||
'use_path_style' => true // Garage supports only path style, must be set to true
|
||||
],
|
||||
],
|
||||
@@ -135,7 +136,7 @@ bucket but doesn't also know the secret encryption key.
|
||||
*Click on the picture to zoom*
|
||||
|
||||
Add a new external storage. Put what you want in "folder name" (eg. "shared"). Select "Amazon S3". Keep "Access Key" for the Authentication field.
|
||||
In Configuration, put your bucket name (eg. nextcloud), the host (eg. 127.0.0.1), the port (eg. 3900 or 443), the region (garage). Tick the SSL box if you have put an HTTPS proxy in front of garage. You must tick the "Path access" box and you must leave the "Legacy authentication (v2)" box empty. Put your Key ID (eg. GK...) and your Secret Key in the last two input boxes. Finally click on the tick symbol on the right of your screen.
|
||||
In Configuration, put your bucket name (eg. nextcloud), the host (eg. 127.0.0.1), the port (eg. 3900 or 443), the region ("garage" if you use the default, or the one your configured in your `garage.toml`). Tick the SSL box if you have put an HTTPS proxy in front of garage. You must tick the "Path access" box and you must leave the "Legacy authentication (v2)" box empty. Put your Key ID (eg. GK...) and your Secret Key in the last two input boxes. Finally click on the tick symbol on the right of your screen.
|
||||
|
||||
Now go to your "Files" app and a new "linked folder" has appeared with the name you chose earlier (eg. "shared").
|
||||
|
||||
@@ -191,10 +192,10 @@ garage key create peertube-key
|
||||
|
||||
Keep the Key ID and the Secret key in a pad, they will be needed later.
|
||||
|
||||
We need two buckets, one for normal videos (named peertube-video) and one for webtorrent videos (named peertube-playlist).
|
||||
We need two buckets, one for normal videos (named peertube-videos) and one for webtorrent videos (named peertube-playlists).
|
||||
```bash
|
||||
garage bucket create peertube-videos
|
||||
garage bucket create peertube-playlist
|
||||
garage bucket create peertube-playlists
|
||||
```
|
||||
|
||||
Now we allow our key to read and write on these buckets:
|
||||
@@ -238,7 +239,7 @@ object_storage:
|
||||
# Put localhost only if you have a garage instance running on that node
|
||||
endpoint: 'http://localhost:3900' # or "garage.example.com" if you have TLS on port 443
|
||||
|
||||
# Garage supports only one region for now, named garage
|
||||
# Garage default region is named "garage", edit according to your config
|
||||
region: 'garage'
|
||||
|
||||
credentials:
|
||||
@@ -253,7 +254,7 @@ object_storage:
|
||||
proxify_private_files: false
|
||||
|
||||
streaming_playlists:
|
||||
bucket_name: 'peertube-playlist'
|
||||
bucket_name: 'peertube-playlists'
|
||||
|
||||
# Keep it empty for our example
|
||||
prefix: ''
|
||||
@@ -335,6 +336,7 @@ From the [official Mastodon documentation](https://docs.joinmastodon.org/admin/t
|
||||
|
||||
```bash
|
||||
$ RAILS_ENV=production bin/tootctl media remove --days 3
|
||||
$ RAILS_ENV=production bin/tootctl media remove --days 15 --prune-profiles
|
||||
$ RAILS_ENV=production bin/tootctl media remove-orphans
|
||||
$ RAILS_ENV=production bin/tootctl preview_cards remove --days 15
|
||||
```
|
||||
@@ -353,8 +355,6 @@ Imports: 1.7 KB
|
||||
Settings: 0 Bytes
|
||||
```
|
||||
|
||||
Unfortunately, [old avatars and headers cannot currently be cleaned up](https://github.com/mastodon/mastodon/issues/9567).
|
||||
|
||||
### Migrating your data
|
||||
|
||||
Data migration should be done with an efficient S3 client.
|
||||
@@ -442,7 +442,7 @@ media_storage_providers:
|
||||
store_synchronous: True # do we want to wait that the file has been written before returning?
|
||||
config:
|
||||
bucket: matrix # the name of our bucket, we chose matrix earlier
|
||||
region_name: garage # only "garage" is supported for the region field
|
||||
region_name: garage # "garage" by default, edit according to your cluster config
|
||||
endpoint_url: http://localhost:3900 # the path to the S3 endpoint
|
||||
access_key_id: "GKxxx" # your Key ID
|
||||
secret_access_key: "xxxx" # your Secret Key
|
||||
@@ -568,13 +568,186 @@ The module can then be configured with:
|
||||
Other configuration options can be found in the
|
||||
[configuration YAML file](https://github.com/processone/ejabberd-contrib/blob/master/mod_s3_upload/conf/mod_s3_upload.yml).
|
||||
|
||||
|
||||
## Ente
|
||||
|
||||
Ente is an alternative for Google Photos and Apple Photos. It [can be selfhosted](https://help.ente.io/self-hosting/) and is working fine with Garage as of May 2024.
|
||||
As a first step we need to create a bucket and a key for Ente:
|
||||
|
||||
```bash
|
||||
garage bucket create ente
|
||||
garage key create ente-key
|
||||
# For the CORS setup to work, the key needs to be --owner as well, at least temporarily.
|
||||
garage bucket allow ente --read --write --owner --key ente-key
|
||||
```
|
||||
|
||||
We also need to setup some CORS rules to allow the Ente frontend to access the bucket:
|
||||
|
||||
```bash
|
||||
export CORS='{"CORSRules":[{"AllowedHeaders":["*"],"AllowedMethods":["GET", "PUT", "POST", "DELETE"],"AllowedOrigins":["*"], "ExposeHeaders":["ETag"]}]}'
|
||||
aws s3api put-bucket-cors --bucket ente --cors-configuration $CORS
|
||||
```
|
||||
|
||||
Now we need to configure ente-server to use our bucket. This is explained [in the Ente S3 documentation](https://help.ente.io/self-hosting/guides/external-s3).
|
||||
Prepare a configuration file for ente's backend as `museum.yaml`:
|
||||
|
||||
```yaml
|
||||
credentials-file: /credentials.yaml
|
||||
apps:
|
||||
public-albums: https://albums.example.tld # If you want to use the share album feature
|
||||
internal:
|
||||
hardcoded-ott:
|
||||
local-domain-suffix: "@example.com" # Your domain
|
||||
local-domain-value: 123456 # Custom One-Time Password since we are not sending mail by default
|
||||
key:
|
||||
# WARNING -- You MUST CHANGE the values below
|
||||
# Someone has made an image that can do it for you : https://github.com/EdyTheCow/ente-selfhost/blob/main/images/ente-server-tools/Dockerfile
|
||||
# Simply build it yourself or run docker run --rm ghcr.io/edythecow/ente-server-tools go run tools/gen-random-keys/main.go
|
||||
encryption: yvmG/RnzKrbCb9L3mgsmoxXr9H7i2Z4qlbT0mL3ln4w= # CHANGE THIS VALUE
|
||||
hash: KXYiG07wC7GIgvCSdg+WmyWdXDAn6XKYJtp/wkEU7x573+byBRAYtpTP0wwvi8i/4l37uicX1dVTUzwH3sLZyw== # CHANGE THIS VALUE
|
||||
jwt:
|
||||
secret: i2DecQmfGreG6q1vBj5tCokhlN41gcfS2cjOs9Po-u8= # CHANGE THIS VALUE
|
||||
```
|
||||
|
||||
The full configuration file can be found [here](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml)
|
||||
Then prepare a credentials file as `credentials.yaml`
|
||||
|
||||
```yaml
|
||||
db:
|
||||
host: postgres
|
||||
port: 5432
|
||||
name: <ente_db_name>
|
||||
user: <pguser>
|
||||
password: <pgpass>
|
||||
|
||||
s3:
|
||||
# Override the primary and secondary hot storage. The commented out values
|
||||
# are the defaults.
|
||||
#
|
||||
hot_storage:
|
||||
primary: b2-eu-cen
|
||||
# secondary: wasabi-eu-central-2-v3
|
||||
|
||||
# If true, enable some workarounds to allow us to use a local minio instance
|
||||
# for object storage.
|
||||
#
|
||||
# 1. Disable SSL.
|
||||
# 2. Use "path" style S3 URLs (see `use_path_style_urls` below).
|
||||
# 3. Directly download the file during replication instead of going via the
|
||||
# Cloudflare worker.
|
||||
# 4. Do not specify storage classes when uploading objects (since minio does
|
||||
# not support them, specifically it doesn't support GLACIER).
|
||||
are_local_buckets: true
|
||||
|
||||
# To use "path" style S3 URLs instead of DNS-based bucket access
|
||||
# default to true if you set "are_local_buckets: true"
|
||||
# use_path_style_urls: true
|
||||
|
||||
b2-eu-cen: # Don't change this key, it is hardcoded
|
||||
key: <keyID>
|
||||
secret: <keySecret>
|
||||
endpoint: garage:3900 # publically accessible endpoint of your garage instance
|
||||
region: garage
|
||||
bucket: <yourbucketName>
|
||||
use_path_style: true
|
||||
# you can specify secondary locations, names are hardcoded as well
|
||||
# wasabi-eu-central-2-v3:
|
||||
# scw-eu-fr-v3:
|
||||
|
||||
# and you can also specify a bucket to be used for embeddings, preview etc..
|
||||
# default to the first bucket
|
||||
# derived-storage: wasabi-eu-central-2-derived
|
||||
```
|
||||
|
||||
Finally you can run it with Docker :
|
||||
|
||||
```bash
|
||||
docker run -d --name ente-server --restart unless-stopped -v /path/to/museum.yaml:/museum.yaml -v /path/to/credentials.yaml:/credentials.yaml -p 8080:8080 ghcr.io/ente-io/ente-server
|
||||
```
|
||||
|
||||
For more information on deployment you can check the [ente documentation](https://help.ente.io/self-hosting/)
|
||||
|
||||
## Pixelfed
|
||||
|
||||
[Pixelfed Technical Documentation > Configuration](https://docs.pixelfed.org/technical-documentation/env.html#filesystem)
|
||||
|
||||
## Pleroma
|
||||
|
||||
[Pleroma Documentation > Pleroma.Uploaders.S3](https://docs-develop.pleroma.social/backend/configuration/cheatsheet/#pleromauploaderss3)
|
||||
### Creating your bucket
|
||||
|
||||
This is the usual Garage setup:
|
||||
|
||||
```bash
|
||||
garage key new --name pleroma-key
|
||||
garage bucket create pleroma
|
||||
garage bucket allow pleroma --read --write --owner --key pleroma-key
|
||||
```
|
||||
|
||||
We also need to expose these buckets publicly to serve their content to users:
|
||||
|
||||
```bash
|
||||
garage bucket website --allow pleroma
|
||||
```
|
||||
|
||||
Note the Key ID and Secret Key.
|
||||
|
||||
### Configure Pleroma
|
||||
|
||||
Update your Pleroma configuration like that in `/etc/pleroma/config.exs`.
|
||||
|
||||
```
|
||||
config :pleroma, Pleroma.Upload,
|
||||
uploader: Pleroma.Uploaders.S3,
|
||||
base_url: "https://pleroma.garage.example.tld"
|
||||
|
||||
config :ex_aws, :s3,
|
||||
access_key_id: "GW...",
|
||||
secret_access_key: "XXX",
|
||||
region: "garage",
|
||||
host: "api.garage.example.tld"
|
||||
```
|
||||
|
||||
And restart Pleroma.
|
||||
|
||||
You can found more information in [Pleroma Documentation > Pleroma.Uploaders.S3](https://docs-develop.pleroma.social/backend/configuration/cheatsheet/#pleromauploaderss3)
|
||||
|
||||
### Migrating your data
|
||||
|
||||
Pleroma have an internal migration tool that can encounter some fatal error
|
||||
|
||||
```
|
||||
** (EXIT from #PID<0.98.0>) an exception was raised:
|
||||
** (File.Error) could not stream "/var/lib/pleroma/uploads/09/f8": illegal operation on a directory
|
||||
(elixir 1.17.3) lib/file/stream.ex:100: anonymous fn/3 in Enumerable.File.Stream.reduce/3
|
||||
(elixir 1.17.3) lib/stream.ex:1675: anonymous fn/5 in Stream.resource/3
|
||||
(elixir 1.17.3) lib/stream.ex:1891: Enumerable.Stream.do_each/4
|
||||
(elixir 1.17.3) lib/task/supervised.ex:370: Task.Supervised.stream_reduce/7
|
||||
(elixir 1.17.3) lib/enum.ex:4423: Enum.map/2
|
||||
(ex_aws_s3 2.5.8) lib/ex_aws/s3/upload.ex:141: ExAws.Operation.ExAws.S3.Upload.perform/2
|
||||
(pleroma 2.10.0) lib/pleroma/uploaders/s3.ex:60: Pleroma.Uploaders.S3.put_file/1
|
||||
(pleroma 2.10.0) lib/pleroma/uploaders/uploader.ex:49: Pleroma.Uploaders.Uploader.put_file/2
|
||||
```
|
||||
|
||||
So, use [your best tool](https://garagehq.deuxfleurs.fr/documentation/connect/cli/) to sync `/var/lib/pleroma/uploads/` in your S3.
|
||||
|
||||
Then, to avoid some non existant problem (just in case of), run this command
|
||||
|
||||
```bash
|
||||
while true
|
||||
do
|
||||
rm -vr $(./bin/pleroma_ctl uploads migrate_local S3 2>&1 | grep "could not stream" | awk -F '"' '{print $2}')
|
||||
sleep 5
|
||||
done
|
||||
```
|
||||
|
||||
If you have many files, stop this command sometime and the command bellow (interactive) to delete local
|
||||
file after upload. Then restart the loop.
|
||||
|
||||
```bash
|
||||
./bin/pleroma_ctl uploads migrate_local S3 --delete
|
||||
```
|
||||
|
||||
And *voilà*
|
||||
|
||||
## Lemmy
|
||||
|
||||
|
||||
@@ -161,3 +161,59 @@ kopia repository validate-provider
|
||||
|
||||
You can then run all the standard kopia commands: `kopia snapshot create`, `kopia mount`...
|
||||
Everything should work out-of-the-box.
|
||||
|
||||
## Plakar
|
||||
|
||||
Create your key and bucket on Garage server:
|
||||
|
||||
```bash
|
||||
garage key create my-plakar-key
|
||||
garage bucket create plakar-backups
|
||||
garage bucket allow plakar-backups --read --write --key my-plakar-key
|
||||
…
|
||||
```
|
||||
|
||||
On Plakar server, add your Garage as a storage location:
|
||||
```bash
|
||||
plakar store add garageS3 s3://my-garage.tld/plakar-backups \
|
||||
region=garage # Or as you've specified in garage.toml \
|
||||
access_key=<Key ID from "garage key info my-plakar-key"> \
|
||||
secret_access_key=<Secret key from "garage key info my-plakar-key">
|
||||
```
|
||||
|
||||
Then create the repository.
|
||||
```bash
|
||||
plakar at @garageS3 create -plaintext # Unencrypted
|
||||
# or
|
||||
plakar at @garageS3 create #encrypted
|
||||
```
|
||||
|
||||
If you encrypt your backups (Plakar default), you will need to define a strong passphrase. Do not forget to save your password safely. It will be needed to decrypt your backups.
|
||||
|
||||
|
||||
After the repository has been created, check that everything works as expected (that might give an empty result as no file has been added yet, but no error message):
|
||||
```bash
|
||||
plakar at @garageS3 check
|
||||
```
|
||||
|
||||
Now that everything is configure, you can use Garage as your backups storage. For instance sync it with a local backup storage:
|
||||
```bash
|
||||
$ plakar at ~/backups sync to @garageS3
|
||||
```
|
||||
|
||||
Or list the S3 storage content:
|
||||
```bash
|
||||
$ plakar at @garageS3 ls
|
||||
```
|
||||
|
||||
More information in Plakar documentation: https://www.plakar.io/docs/main/quickstart/
|
||||
|
||||
## Synology HyperBackup
|
||||
|
||||
HyperBackup can be configured to upload backups to garage using a custom S3 destination. However, the HyperBackup client hardcodes the `us-east-1` region that is a critical input to the v4 signature process. If garage is not set to `us-east-1`, HyperBackup will recognize available buckets, but fail during the final setup stage.
|
||||
|
||||
In garage.toml:
|
||||
```toml
|
||||
[s3_api]
|
||||
s3_region = "us-east-1"
|
||||
```
|
||||
|
||||
@@ -149,6 +149,15 @@ rclone help
|
||||
This will tremendously accelerate operations such as `rclone sync` or `rclone ncdu` by reducing the number
|
||||
of ListObjects calls that are made.
|
||||
|
||||
**Garage behind Cloudflare proxy:** when running Garage behind Cloudflare proxy, you might see `Response: error 403 Forbidden, Forbidden: Invalid signature` error in your garage logs or `AccessDenied: Forbidden: Invalid signature` error in rclone logs. Try adding `--s3-sign-accept-encoding=false` flag to your rclone command and see if the issue is resolved.
|
||||
|
||||
```bash
|
||||
# this throws an error
|
||||
rclone lsd garage:
|
||||
|
||||
# this should work
|
||||
rclone lsd --s3-sign-accept-encoding=false garage:
|
||||
```
|
||||
|
||||
## `s3cmd`
|
||||
|
||||
@@ -314,4 +323,3 @@ ls
|
||||
```
|
||||
|
||||
And through the web interface at http://[::1]:8080/web/client
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Garage can also help you serve this content.
|
||||
|
||||
## Gitea
|
||||
|
||||
You can use Garage with Gitea to store your [git LFS](https://git-lfs.github.com/) data, your users' avatar, and their attachements.
|
||||
You can use Garage with Gitea to store your [git LFS](https://git-lfs.github.com/) data, your users' avatar, and their attachments.
|
||||
You can configure a different target for each data type (check `[lfs]` and `[attachment]` sections of the Gitea documentation) and you can provide a default one through the `[storage]` section.
|
||||
|
||||
Let's start by creating a key and a bucket (your key id and secret will be needed later, keep them somewhere):
|
||||
|
||||
@@ -8,18 +8,18 @@ have published Ansible roles. We list them and compare them below.
|
||||
|
||||
## Comparison of Ansible roles
|
||||
|
||||
| Feature | [ansible-role-garage](#zorun-ansible-role-garage) | [garage-docker-ansible-deploy](#moan0s-garage-docker-ansible-deploy) |
|
||||
|------------------------------------|---------------------------------------------|---------------------------------------------------------------|
|
||||
| **Runtime** | Systemd | Docker |
|
||||
| **Target OS** | Any Linux | Any Linux |
|
||||
| **Architecture** | amd64, arm64, i686 | amd64, arm64 |
|
||||
| **Additional software** | None | Traefik |
|
||||
| **Automatic node connection** | ❌ | ✅ |
|
||||
| **Layout management** | ❌ | ✅ |
|
||||
| **Manage buckets & keys** | ❌ | ✅ (basic) |
|
||||
| **Allow custom Garage config** | ✅ | ❌ |
|
||||
| **Facilitate Garage upgrades** | ✅ | ❌ |
|
||||
| **Multiple instances on one host** | ✅ | ✅ |
|
||||
| Feature | [ansible-role-garage](#zorun-ansible-role-garage) | [garage-docker-ansible-deploy](#moan0s-garage-docker-ansible-deploy) | [eddster2309 ansible-role-garage](#eddster2309-ansible-role-garage) |
|
||||
|------------------------------------|---------------------------------------------|---------------------------------------------------------------|---------------------------------|
|
||||
| **Runtime** | Systemd | Docker | Systemd |
|
||||
| **Target OS** | Any Linux | Any Linux | Any Linux |
|
||||
| **Architecture** | amd64, arm64, i686 | amd64, arm64 | arm64, arm, 386, amd64 |
|
||||
| **Additional software** | None | Traefik | Ngnix and Keepalived (optional) |
|
||||
| **Automatic node connection** | ❌ | ✅ | ✅ |
|
||||
| **Layout management** | ❌ | ✅ | ✅ |
|
||||
| **Manage buckets & keys** | ❌ | ✅ (basic) | ✅ |
|
||||
| **Allow custom Garage config** | ✅ | ❌ | ❌ |
|
||||
| **Facilitate Garage upgrades** | ✅ | ❌ | ✅ |
|
||||
| **Multiple instances on one host** | ✅ | ✅ | ❌ |
|
||||
|
||||
|
||||
## zorun/ansible-role-garage
|
||||
@@ -49,3 +49,15 @@ structured DNS names, etc).
|
||||
|
||||
As a result, this role makes it easier to start with Garage on Ansible,
|
||||
but is less flexible.
|
||||
|
||||
## eddster2309/ansible-role-garage
|
||||
|
||||
[Source code](https://github.com/eddster2309/ansible-role-garage), [Ansible galaxy](https://galaxy.ansible.com/ui/standalone/roles/eddster2309/garage/)
|
||||
|
||||
This role is a opinionated but customisable role using the official Garage
|
||||
static binaries and only requires Systemd. As such it should work on any
|
||||
Linux based host. It includes all the nesscary configuration to
|
||||
automatically setup a clustered Garage deployment. Most Garage
|
||||
configuration options are exposed through Ansible variables so while you
|
||||
can't provide a custom config you can get very close. It can optionally
|
||||
installed a HA nginx deployment with Keepalived.
|
||||
|
||||
@@ -15,9 +15,10 @@ Alpine Linux repositories (available since v3.17):
|
||||
apk add garage
|
||||
```
|
||||
|
||||
The default configuration file is installed to `/etc/garage.toml`. You can run
|
||||
Garage using: `rc-service garage start`. If you don't specify `rpc_secret`, it
|
||||
will be automatically replaced with a random string on the first start.
|
||||
The default configuration file is installed to `/etc/garage/garage.toml`. You can run
|
||||
Garage using: `rc-service garage start`.
|
||||
|
||||
If you don't specify `rpc_secret`, it will be automatically replaced with a random string on the first start.
|
||||
|
||||
Please note that this package is built without Consul discovery, Kubernetes
|
||||
discovery, OpenTelemetry exporter, and K2V features (K2V will be enabled once
|
||||
@@ -26,7 +27,11 @@ it's stable).
|
||||
|
||||
## Arch Linux
|
||||
|
||||
Garage is available in the [AUR](https://aur.archlinux.org/packages/garage).
|
||||
Garage is available in the official repositories under [extra](https://archlinux.org/packages/extra/x86_64/garage).
|
||||
|
||||
```bash
|
||||
pacman -S garage
|
||||
```
|
||||
|
||||
## FreeBSD
|
||||
|
||||
@@ -39,3 +44,9 @@ pkg install garage
|
||||
```bash
|
||||
nix-shell -p garage
|
||||
```
|
||||
|
||||
## conda-forge
|
||||
|
||||
```bash
|
||||
pixi global install garage
|
||||
```
|
||||
|
||||
@@ -20,10 +20,10 @@ sudo apt-get update
|
||||
sudo apt-get install build-essential
|
||||
```
|
||||
|
||||
## Building from source from the Gitea repository
|
||||
## Building from source from the Forgejo repository
|
||||
|
||||
The primary location for Garage's source code is the
|
||||
[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage),
|
||||
[Forgejo repository](https://git.deuxfleurs.fr/Deuxfleurs/garage),
|
||||
which contains all of the released versions as well as the code
|
||||
for the developpement of the next version.
|
||||
|
||||
@@ -85,11 +85,14 @@ The following feature flags are available in v0.8.0:
|
||||
| Feature flag | Enabled | Description |
|
||||
| ------------ | ------- | ----------- |
|
||||
| `bundled-libs` | *by default* | Use bundled version of sqlite3, zstd, lmdb and libsodium |
|
||||
| `system-libs` | optional | Use system version of sqlite3, zstd, lmdb and libsodium<br>if available (exclusive with `bundled-libs`, build using<br>`cargo build --no-default-features --features system-libs`) |
|
||||
| `consul-discovery` | optional | Enable automatic registration and discovery<br>of cluster nodes through the Consul API |
|
||||
| `fjall` | experimental | Enable using Fjall to store Garage's metadata |
|
||||
| `journald` | optional | Enable logging to systemd-journald with<br>`GARAGE_LOG_TO_JOURNALD=true` environment variable set |
|
||||
| `k2v` | optional | Enable the experimental K2V API (if used, all nodes on your<br>Garage cluster must have it enabled as well) |
|
||||
| `kubernetes-discovery` | optional | Enable automatic registration and discovery<br>of cluster nodes through the Kubernetes API |
|
||||
| `metrics` | *by default* | Enable collection of metrics in Prometheus format on the admin API |
|
||||
| `telemetry-otlp` | optional | Enable collection of execution traces using OpenTelemetry |
|
||||
| `syslog` | optional | Enable logging to Syslog |
|
||||
| `lmdb` | *by default* | Enable using LMDB to store Garage's metadata |
|
||||
| `metrics` | *by default* | Enable collection of metrics in Prometheus format on the admin API |
|
||||
| `sqlite` | *by default* | Enable using Sqlite3 to store Garage's metadata |
|
||||
| `syslog` | optional | Enable logging to Syslog with<br>`GARAGE_LOG_TO_SYSLOG=true` environment variable set |
|
||||
| `system-libs` | optional | Use system version of sqlite3, zstd, lmdb and libsodium<br>if available (exclusive with `bundled-libs`, build using<br>`cargo build --no-default-features --features system-libs`) |
|
||||
| `telemetry-otlp` | optional | Enable collection of execution traces using OpenTelemetry |
|
||||
|
||||
@@ -11,7 +11,7 @@ Firstly clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://git.deuxfleurs.fr/Deuxfleurs/garage
|
||||
cd garage/scripts/helm
|
||||
cd garage/script/helm
|
||||
```
|
||||
|
||||
Deploy with default options:
|
||||
@@ -26,6 +26,13 @@ Or deploy with custom values:
|
||||
helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml
|
||||
```
|
||||
|
||||
If you want to manage the CustomRessourceDefinition used by garage for its `kubernetes_discovery` outside of the helm chart, add `garage.kubernetesSkipCrd: true` to your custom values and use the kustomization before deploying the helm chart:
|
||||
|
||||
```bash
|
||||
kubectl apply -k ../k8s/crd
|
||||
helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml
|
||||
```
|
||||
|
||||
After deploying, cluster layout must be configured manually as described in [Creating a cluster layout](@/documentation/quick-start/_index.md#creating-a-cluster-layout). Use the following command to access garage CLI:
|
||||
|
||||
```bash
|
||||
@@ -45,7 +52,7 @@ This is an example `values.overrride.yaml` for deploying in a microk8s cluster w
|
||||
```yaml
|
||||
garage:
|
||||
# Use only 2 replicas per object
|
||||
replicationMode: "2"
|
||||
replicationFactor: 2
|
||||
|
||||
# Start 4 instances (StatefulSets) of garage
|
||||
deployment:
|
||||
@@ -86,3 +93,62 @@ helm delete --namespace garage garage
|
||||
```
|
||||
|
||||
Note that this will leave behind custom CRD `garagenodes.deuxfleurs.fr`, which must be removed manually if desired.
|
||||
|
||||
## Increase PVC size on running Garage instances
|
||||
|
||||
Since the Garage Helm chart creates the data and meta PVC based on `StatefulSet` templates, increasing the PVC size can be a bit tricky.
|
||||
|
||||
### Confirm the `StorageClass` used for Garage supports volume expansion
|
||||
|
||||
Confirm the storage class used for garage.
|
||||
|
||||
```bash
|
||||
kubectl -n garage get pvc
|
||||
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
|
||||
data-garage-0 Bound pvc-080360c9-8ce3-4acf-8579-1701e57b7f3f 30Gi RWO longhorn-local <unset> 77d
|
||||
data-garage-1 Bound pvc-ab8ba697-6030-4fc7-ab3c-0d6df9e3dbc0 30Gi RWO longhorn-local <unset> 5d8h
|
||||
data-garage-2 Bound pvc-3ab37551-0231-4604-986d-136d0fd950ec 30Gi RWO longhorn-local <unset> 5d5h
|
||||
meta-garage-0 Bound pvc-3b457302-3023-4169-846e-c928c5f2ea65 3Gi RWO longhorn-local <unset> 77d
|
||||
meta-garage-1 Bound pvc-49ace2b9-5c85-42df-9247-51c4cf64b460 3Gi RWO longhorn-local <unset> 5d8h
|
||||
meta-garage-2 Bound pvc-99e2e50f-42b4-4128-ae2f-b52629259723 3Gi RWO longhorn-local <unset> 5d5h
|
||||
```
|
||||
|
||||
In this case, the storage class is `longhorn-local`. Now, check if `ALLOWVOLUMEEXPANSION` is true for the used `StorageClass`.
|
||||
|
||||
```bash
|
||||
kubectl get storageclasses.storage.k8s.io longhorn-local
|
||||
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
|
||||
longhorn-local driver.longhorn.io Delete Immediate true 103d
|
||||
```
|
||||
|
||||
If your `StorageClass` does not support volume expansion, double check if you can enable it. Otherwise, your only real option is to spin up a new Garage cluster with increased size and migrate all data over.
|
||||
|
||||
If your `StorageClass` supports expansion, you are free to continue.
|
||||
|
||||
### Increase the size of the PVCs
|
||||
|
||||
Increase the size of all PVCs to your desired size.
|
||||
|
||||
```bash
|
||||
kubectl -n garage edit pvc data-garage-0
|
||||
kubectl -n garage edit pvc data-garage-1
|
||||
kubectl -n garage edit pvc data-garage-2
|
||||
kubectl -n garage edit pvc meta-garage-0
|
||||
kubectl -n garage edit pvc meta-garage-1
|
||||
kubectl -n garage edit pvc meta-garage-2
|
||||
```
|
||||
|
||||
### Increase the size of the `StatefulSet` PVC template
|
||||
|
||||
This is an optional step, but if not done, future instances of Garage will be created with the original size from the template.
|
||||
|
||||
```bash
|
||||
kubectl -n garage delete sts --cascade=orphan garage
|
||||
statefulset.apps "garage" deleted
|
||||
```
|
||||
|
||||
This will remove the Garage `StatefulSet` but leave the pods running. It may seem destructive but needs to be done this way since edits to the size of PVC templates are prohibited.
|
||||
|
||||
### Redeploy the `StatefulSet`
|
||||
|
||||
Now the size of future PVCs can be increased, and the Garage Helm chart can be upgraded. The new `StatefulSet` should take ownership of the orphaned pods again.
|
||||
|
||||
@@ -96,14 +96,14 @@ to store 2 TB of data in total.
|
||||
## Get a Docker image
|
||||
|
||||
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
||||
We encourage you to use a fixed tag (eg. `v1.0.0`) and not the `latest` tag.
|
||||
For this example, we will use the latest published version at the time of the writing which is `v1.0.0` but it's up to you
|
||||
We encourage you to use a fixed tag (eg. `v2.2.0`) and not the `latest` tag.
|
||||
For this example, we will use the latest published version at the time of the writing which is `v2.2.0` but it's up to you
|
||||
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
sudo docker pull dxflrs/garage:v1.0.0
|
||||
sudo docker pull dxflrs/garage:v2.2.0
|
||||
```
|
||||
|
||||
## Deploying and configuring Garage
|
||||
@@ -152,6 +152,8 @@ Check the following for your configuration files:
|
||||
- Make sure `rpc_public_addr` contains the public IP address of the node you are configuring.
|
||||
This parameter is optional but recommended: if your nodes have trouble communicating with
|
||||
one another, consider adding it.
|
||||
Alternatively, you can also set `rpc_public_addr_subnet`, which can filter
|
||||
the addresses announced to other peers to a specific subnet.
|
||||
|
||||
- Make sure `rpc_secret` is the same value on all nodes. It should be a 32-bytes hex-encoded secret key.
|
||||
You can generate such a key with `openssl rand -hex 32`.
|
||||
@@ -169,7 +171,7 @@ docker run \
|
||||
-v /etc/garage.toml:/etc/garage.toml \
|
||||
-v /var/lib/garage/meta:/var/lib/garage/meta \
|
||||
-v /var/lib/garage/data:/var/lib/garage/data \
|
||||
dxflrs/garage:v1.0.0
|
||||
dxflrs/garage:v2.2.0
|
||||
```
|
||||
|
||||
With this command line, Garage should be started automatically at each boot.
|
||||
@@ -183,7 +185,7 @@ If you want to use `docker-compose`, you may use the following `docker-compose.y
|
||||
version: "3"
|
||||
services:
|
||||
garage:
|
||||
image: dxflrs/garage:v1.0.0
|
||||
image: dxflrs/garage:v2.2.0
|
||||
network_mode: "host"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
||||
@@ -7,7 +7,7 @@ The main reason to add a reverse proxy in front of Garage is to provide TLS to y
|
||||
|
||||
In production you will likely need your certificates signed by a certificate authority.
|
||||
The most automated way is to use a provider supporting the [ACME protocol](https://datatracker.ietf.org/doc/html/rfc8555)
|
||||
such as [Let's Encrypt](https://letsencrypt.org/), [ZeroSSL](https://zerossl.com/) or [Buypass Go SSL](https://www.buypass.com/ssl/products/acme).
|
||||
such as [Let's Encrypt](https://letsencrypt.org/) or [ZeroSSL](https://zerossl.com/).
|
||||
|
||||
If you are only testing Garage, you can generate a self-signed certificate to follow the documentation:
|
||||
|
||||
@@ -97,7 +97,7 @@ server {
|
||||
location / {
|
||||
proxy_pass http://s3_backend;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Host $http_host;
|
||||
# Disable buffering to a temporary file.
|
||||
proxy_max_temp_file_size 0;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ StateDirectory=garage
|
||||
DynamicUser=true
|
||||
ProtectHome=true
|
||||
NoNewPrivileges=true
|
||||
LimitNOFILE=42000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -50,3 +50,22 @@ locations. They use Garage themselves for the following tasks:
|
||||
|
||||
The Deuxfleurs Garage cluster is a multi-site cluster currently composed of
|
||||
9 nodes in 3 physical locations.
|
||||
|
||||
### Triplebit
|
||||
|
||||
[Triplebit](https://www.triplebit.org) is a non-profit hosting provider and
|
||||
ISP focused on improving access to privacy-related services. They use
|
||||
Garage themselves for the following tasks:
|
||||
|
||||
- Hosting of their homepage, [privacyguides.org](https://www.privacyguides.org/), and various other static sites
|
||||
|
||||
- As a PowerDNS authoritative zone backend through [Lightning Stream](https://doc.powerdns.com/lightningstream/latest/index.html) and [LMDB](https://doc.powerdns.com/authoritative/backends/lmdb.html)
|
||||
|
||||
- As a Mastodon media storage backend for [mstdn.party](https://mstdn.party/) and [mstdn.plus](https://mstdn.plus/)
|
||||
|
||||
- As a PeerTube storage backend for [neat.tube](https://neat.tube/)
|
||||
|
||||
- As a [Matrix media backend](https://github.com/matrix-org/synapse-s3-storage-provider)
|
||||
|
||||
Triplebit's Garage cluster is a multi-site cluster currently composed of
|
||||
15 storage nodes in 3 physical locations.
|
||||
|
||||
@@ -67,7 +67,7 @@ Pithos has been abandonned and should probably not used yet, in the following we
|
||||
Pithos was relying as a S3 proxy in front of Cassandra (and was working with Scylla DB too).
|
||||
From its designers' mouth, storing data in Cassandra has shown its limitations justifying the project abandonment.
|
||||
They built a closed-source version 2 that does not store blobs in the database (only metadata) but did not communicate further on it.
|
||||
We considered there v2's design but concluded that it does not fit both our *Self-contained & lightweight* and *Simple* properties. It makes the development, the deployment and the operations more complicated while reducing the flexibility.
|
||||
We considered their v2's design but concluded that it does not fit both our *Self-contained & lightweight* and *Simple* properties. It makes the development, the deployment and the operations more complicated while reducing the flexibility.
|
||||
|
||||
**[Riak CS](https://docs.riak.com/riak/cs/2.1.1/index.html):**
|
||||
*Not written yet*
|
||||
|
||||
@@ -36,7 +36,7 @@ sudo killall nix-daemon
|
||||
Now you can enter our nix-shell, all the required packages will be downloaded but they will not pollute your environment outside of the shell:
|
||||
|
||||
```bash
|
||||
nix-shell
|
||||
nix-shell -A devShell
|
||||
```
|
||||
|
||||
You can use the traditional Rust development workflow:
|
||||
@@ -65,8 +65,8 @@ nix-build -j $(nproc) --max-jobs auto
|
||||
```
|
||||
|
||||
Our build has multiple parameters you might want to set:
|
||||
- `release` build with release optimisations instead of debug
|
||||
- `target allows` for cross compilation
|
||||
- `release` to build with release optimisations instead of debug
|
||||
- `target` allows for cross compilation
|
||||
- `compileMode` can be set to test or bench to build a unit test runner
|
||||
- `git_version` to inject the hash to display when running `garage stats`
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ You may pause an ongoing scrub using `garage repair scrub pause`, but note that
|
||||
the scrub will resume automatically 24 hours later as Garage will not let your
|
||||
cluster run without a regular scrub. If the scrub procedure is too intensive
|
||||
for your servers and is slowing down your workload, the recommended solution
|
||||
is to increase the "scrub tranquility" using `garage repair scrub set-tranquility`.
|
||||
is to increase the "scrub tranquility" using `garage worker set scrub-tranquility`.
|
||||
A higher tranquility value will make Garage take longer pauses between two block
|
||||
verifications. Of course, scrubbing the entire data store will also take longer.
|
||||
|
||||
|
||||
@@ -21,14 +21,14 @@ data_dir = [
|
||||
```
|
||||
|
||||
Garage will automatically balance all blocks stored by the node
|
||||
among the different specified directories, proportionnally to the
|
||||
among the different specified directories, proportionally to the
|
||||
specified capacities.
|
||||
|
||||
## Updating the list of storage locations
|
||||
|
||||
If you add new storage locations to your `data_dir`,
|
||||
Garage will not rebalance existing data between storage locations.
|
||||
Newly written blocks will be balanced proportionnally to the specified capacities,
|
||||
Newly written blocks will be balanced proportionally to the specified capacities,
|
||||
and existing data may be moved between drives to improve balancing,
|
||||
but only opportunistically when a data block is re-written (e.g. an object
|
||||
is re-uploaded, or an object with a duplicate block is uploaded).
|
||||
|
||||
@@ -5,7 +5,7 @@ weight = 40
|
||||
|
||||
Garage is meant to work on old, second-hand hardware.
|
||||
In particular, this makes it likely that some of your drives will fail, and some manual intervention will be needed.
|
||||
Fear not! For Garage is fully equipped to handle drive failures, in most common cases.
|
||||
Fear not! Garage is fully equipped to handle drive failures, in most common cases.
|
||||
|
||||
## A note on availability of Garage
|
||||
|
||||
@@ -61,7 +61,7 @@ garage repair -a --yes blocks
|
||||
|
||||
This will re-synchronize blocks of data that are missing to the new HDD, reading them from copies located on other nodes.
|
||||
|
||||
You can check on the advancement of this process by doing the following command:
|
||||
You can check on the advancement of this process by doing the following command:
|
||||
|
||||
```bash
|
||||
garage stats -a
|
||||
@@ -161,4 +161,7 @@ your recovery options are as follows:
|
||||
|
||||
- **Option 3: restoring a filesystem-level snapshot.** If you are using ZFS or
|
||||
BTRFS to snapshot your metadata partition, refer to their specific
|
||||
documentation on rolling back or copying files from an old snapshot.
|
||||
documentation on rolling back or copying files from an old snapshot.
|
||||
Note that, depending on the properties of the filesystem and of the DB engine,
|
||||
if these snapshots were taken during a write operation to the database, they may
|
||||
also be corrupted and thus unfit for recovery.
|
||||
|
||||
@@ -71,7 +71,7 @@ The entire procedure would look something like this:
|
||||
|
||||
2. Take each node offline individually to back up its metadata folder, bring them back online once the backup is done.
|
||||
You can do all of the nodes in a single zone at once as that won't impact global cluster availability.
|
||||
Do not try to make a backup of the metadata folder of a running node.
|
||||
Do not try to manually copy the metadata folder of a running node.
|
||||
|
||||
**Since Garage v0.9.4,** you can use the `garage meta snapshot --all` command
|
||||
to take a simultaneous snapshot of the metadata database files of all your
|
||||
|
||||
@@ -42,6 +42,13 @@ If a binary of the last version is not available for your architecture,
|
||||
or if you want a build customized for your system,
|
||||
you can [build Garage from source](@/documentation/cookbook/from-source.md).
|
||||
|
||||
If none of these option work for you, you can also run Garage in a Docker
|
||||
container. When using Docker, the commands used in this guide will not work
|
||||
anymore. We recommend reading the tutorial on [configuring a
|
||||
multi-node cluster](@/documentation/cookbook/real-world.md) to learn about
|
||||
using Garage as a Docker container. For simplicity, a minimal command to launch
|
||||
Garage using Docker is provided in this quick start guide as well.
|
||||
|
||||
|
||||
## Configuring and starting Garage
|
||||
|
||||
@@ -85,6 +92,9 @@ metrics_token = "$(openssl rand -base64 32)"
|
||||
EOF
|
||||
```
|
||||
|
||||
See the [Configuration file format](https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/)
|
||||
for complete options and values.
|
||||
|
||||
Now that your configuration file has been created, you may save it to the directory of your choice.
|
||||
By default, Garage looks for **`/etc/garage.toml`.**
|
||||
You can also store it somewhere else, but you will have to specify `-c path/to/garage.toml`
|
||||
@@ -111,6 +121,26 @@ garage -c path/to/garage.toml server
|
||||
|
||||
If you have placed the `garage.toml` file in `/etc` (its default location), you can simply run `garage server`.
|
||||
|
||||
Alternatively, if you cannot or do not wish to run the Garage binary directly,
|
||||
you may use Docker to run Garage in a container using the following command:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-d \
|
||||
--name garaged \
|
||||
-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903 \
|
||||
-v /path/to/garage.toml:/etc/garage.toml \
|
||||
-v /path/to/garage/meta:/var/lib/garage/meta \
|
||||
-v /path/to/garage/data:/var/lib/garage/data \
|
||||
dxflrs/garage:v2.2.0
|
||||
```
|
||||
|
||||
Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
Ensure your configuration file, `metadata_dir` and `data_dir` are readable by the user running the `garage` server or Docker.
|
||||
|
||||
You can tune Garage's verbosity by setting the `RUST_LOG=` environment variable. \
|
||||
Available log levels are (from less verbose to more verbose): `error`, `warn`, `info` *(default)*, `debug` and `trace`.
|
||||
|
||||
@@ -131,6 +161,9 @@ It uses values from the TOML configuration file to find the Garage daemon runnin
|
||||
local node, therefore if your configuration file is not at `/etc/garage.toml` you will
|
||||
again have to specify `-c path/to/garage.toml` at each invocation.
|
||||
|
||||
If you are running Garage in a Docker container, you can set `alias garage="docker exec -ti <container name> /garage"`
|
||||
to use the Garage binary inside your container.
|
||||
|
||||
If the `garage` CLI is able to correctly detect the parameters of your local Garage node,
|
||||
the following command should be enough to show the status of your cluster:
|
||||
|
||||
@@ -149,11 +182,12 @@ ID Hostname Address Tag Zone Capacit
|
||||
## Creating a cluster layout
|
||||
|
||||
Creating a cluster layout for a Garage deployment means informing Garage
|
||||
of the disk space available on each node of the cluster
|
||||
as well as the zone (e.g. datacenter) each machine is located in.
|
||||
of the disk space available on each node of the cluster, `-c`,
|
||||
as well as the name of the zone (e.g. datacenter), `-z`, each machine is located in.
|
||||
|
||||
For our test deployment, we are using only one node. The way in which we configure
|
||||
it does not matter, you can simply write:
|
||||
For our test deployment, we are have only one node with zone named `dc1` and a
|
||||
capacity of `1G`, though the capacity is ignored for a single node deployment
|
||||
and can be changed later when adding new nodes.
|
||||
|
||||
```bash
|
||||
garage layout assign -z dc1 -c 1G <node_id>
|
||||
@@ -166,7 +200,7 @@ For instance here you could write just `garage layout assign -z dc1 -c 1G 563e`.
|
||||
The layout then has to be applied to the cluster, using:
|
||||
|
||||
```bash
|
||||
garage layout apply
|
||||
garage layout apply --version 1
|
||||
```
|
||||
|
||||
|
||||
@@ -316,7 +350,7 @@ Check [our s3 compatibility list](@/documentation/reference-manual/s3-compatibil
|
||||
|
||||
### 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 receive files from/to Garage:
|
||||
|
||||
- [minio-client](@/documentation/connect/cli.md#minio-client)
|
||||
- [s3cmd](@/documentation/connect/cli.md#s3cmd)
|
||||
|
||||
@@ -6,41 +6,167 @@ weight = 40
|
||||
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))
|
||||
reference](@/documentation/reference-manual/configuration.md)).
|
||||
|
||||
**WARNING.** At this point, there is no commitment to the stability of the APIs described in this document.
|
||||
We will bump the version numbers prefixed to each API endpoint each time the syntax
|
||||
or semantics change, meaning that code that relies on these endpoint will break
|
||||
when changes are introduced.
|
||||
|
||||
Versions:
|
||||
- Before Garage 0.7.2 - no admin API
|
||||
- Garage 0.7.2 - admin APIv0
|
||||
- Garage 0.9.0 - admin APIv1, deprecate admin APIv0
|
||||
The current version of the admin API is v2. No breaking changes to the Garage
|
||||
administration API will be published outside of a major release.
|
||||
|
||||
History of previous versions:
|
||||
|
||||
- Before Garage v0.7.2 - no admin API
|
||||
- Garage v0.7.2 - admin API v0
|
||||
- Garage v0.9.0 - admin API v1, deprecate admin API v0
|
||||
- Garage v2.0.0 - admin API v2, deprecate admin API v1
|
||||
|
||||
## Access control
|
||||
|
||||
The admin API uses two different tokens for access control, that are specified in the config file's `[admin]` section:
|
||||
### Using an API token
|
||||
|
||||
- `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:
|
||||
Administration API tokens 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
|
||||
### User-defined API tokens
|
||||
|
||||
Cluster administrators may dynamically define administration tokens using the CLI commands under `garage admin-token`.
|
||||
Such tokens may be limited in scope, meaning that they may enable access to only a subset of API calls.
|
||||
They may also have an expiration date to limit their use in time.
|
||||
|
||||
Here is an example to create an administration token that is valid for 30 days
|
||||
and gives access to only a subset of API calls, allowing it to create buckets
|
||||
and access keys and give keys permissions on buckets:
|
||||
|
||||
```bash
|
||||
$ garage admin-token create --expires-in 30d \
|
||||
--scope ListBuckets,GetBucketInfo,ListKeys,GetKeyInfo,CreateBucket,CreateKey,AllowBucketKey,DenyBucketKey \
|
||||
my-token
|
||||
This is your secret bearer token, it will not be shown again by Garage:
|
||||
|
||||
8ed1830b10a276ff57061950.kOSIpxWK9zSGbTO9Xadpv3YndSFWma0_snXcYHaORXk
|
||||
|
||||
==== ADMINISTRATION TOKEN INFORMATION ====
|
||||
Token ID: 8ed1830b10a276ff57061950
|
||||
Token name: my-token
|
||||
Created: 2025-06-15 15:12:44.160 +02:00
|
||||
Validity: valid
|
||||
Expiration: 2025-07-15 15:12:44.117 +02:00
|
||||
|
||||
Scope: ListBuckets
|
||||
GetBucketInfo
|
||||
ListKeys
|
||||
GetKeyInfo
|
||||
CreateBucket
|
||||
CreateKey
|
||||
AllowBucketKey
|
||||
DenyBucketKey
|
||||
```
|
||||
|
||||
When running this command, your token will be shown only once and **will never
|
||||
be shown again by Garage**, so make sure to save it directly. The token is
|
||||
hashed internally, and is identified by its prefix (32 hex digits followed by a
|
||||
dot) which is saved in clear.
|
||||
|
||||
When running `garage admin-token list`, you might see something like this:
|
||||
|
||||
```
|
||||
ID Created Name Expiration Scope
|
||||
- - metrics_token (from daemon configuration) never Metrics
|
||||
8ed1830b10a276ff57061950 2025-06-15 my-token 2025-07-15 15:12:44.117 +02:00 ListBuckets, ... (8)
|
||||
```
|
||||
|
||||
### Master API tokens
|
||||
|
||||
The admin API can also use two different master tokens for access control,
|
||||
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 only possible with a user-defined admin token.
|
||||
|
||||
With the introduction of multiple user-defined admin tokens, the use of master
|
||||
API tokens is now discouraged.
|
||||
|
||||
|
||||
## Using the admin API
|
||||
|
||||
All of the admin API endpoints are described in the OpenAPI specification:
|
||||
|
||||
- APIv2 - [HTML spec](https://garagehq.deuxfleurs.fr/api/garage-admin-v2.html) - [OpenAPI JSON](https://garagehq.deuxfleurs.fr/api/garage-admin-v2.json)
|
||||
- APIv1 (deprecated) - [HTML spec](https://garagehq.deuxfleurs.fr/api/garage-admin-v1.html) - [OpenAPI YAML](https://garagehq.deuxfleurs.fr/api/garage-admin-v1.yml)
|
||||
- APIv0 (deprecated) - [HTML spec](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.html) - [OpenAPI YAML](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.yml)
|
||||
|
||||
Making a request to the API from the command line can be as simple as running:
|
||||
|
||||
```bash
|
||||
curl -H 'Authorization: Bearer s3cr3t' http://localhost:3903/v2/GetClusterStatus | jq
|
||||
```
|
||||
|
||||
For more advanced use cases, we recommend using an SDK.
|
||||
[Go to the "Build your own app" section to know how to use our SDKs](@/documentation/build/_index.md)
|
||||
|
||||
### Making API calls from the `garage` CLI
|
||||
|
||||
Since v2.0.0, the `garage` binary provides a subcommand `garage json-api` that
|
||||
allows you to invoke the API without making an HTTP request. This can be
|
||||
useful for scripting Garage deployments.
|
||||
|
||||
`garage json-api` proxies API calls through Garage's internal RPC protocol,
|
||||
therefore it does not require any form of authentication: RPC connection
|
||||
parameters are discovered automatically to contact the locally-running Garage
|
||||
instance (as when running any other `garage` CLI command).
|
||||
|
||||
For simple calls that take no parameters, usage is as follows:
|
||||
|
||||
```
|
||||
$ garage json-api GetClusterHealth
|
||||
{
|
||||
"connectedNodes": 3,
|
||||
"knownNodes": 3,
|
||||
"partitions": 256,
|
||||
"partitionsAllOk": 256,
|
||||
"partitionsQuorum": 256,
|
||||
"status": "healthy",
|
||||
"storageNodes": 3,
|
||||
"storageNodesOk": 3
|
||||
}
|
||||
```
|
||||
|
||||
If you need to specify a JSON body for your call, you can add it directly after
|
||||
the name of the function you are calling:
|
||||
|
||||
```
|
||||
$ garage json-api CreateAdminToken '{"name": "test"}'
|
||||
```
|
||||
|
||||
Or you can feed it through stdin by adding a `-` as the last command parameter:
|
||||
|
||||
```
|
||||
$ garage json-api CreateAdminToken -
|
||||
{"name": "test"}
|
||||
<EOF>
|
||||
```
|
||||
|
||||
For admin API calls that would have taken query parameters in their HTTP version, these parameters can be passed in the JSON body object:
|
||||
|
||||
```
|
||||
$ garage json-api GetAdminTokenInfo '{"id":"b0e6e0ace2c0b2aca4cdb2de"}'
|
||||
```
|
||||
|
||||
For admin API calls that take both query parameters and a JSON body, combine them in the following fashion:
|
||||
|
||||
```
|
||||
$ garage json-api UpdateAdminToken '{"id":"b0e6e0ace2c0b2aca4cdb2de", "body":{"name":"not a test"}}'
|
||||
```
|
||||
|
||||
## Special administration API endpoints
|
||||
|
||||
### Metrics `GET /metrics`
|
||||
|
||||
@@ -83,7 +209,7 @@ content-length: 102
|
||||
date: Tue, 08 Aug 2023 07:22:38 GMT
|
||||
|
||||
Garage is fully operational
|
||||
Consult the full health check API endpoint at /v0/health for more details
|
||||
Consult the full health check API endpoint at /v2/GetClusterHealth for more details
|
||||
```
|
||||
|
||||
### On-demand TLS `GET /check`
|
||||
@@ -126,23 +252,7 @@ $ curl -so /dev/null -w "%{http_code}" http://localhost:3903/check?domain=exampl
|
||||
200
|
||||
```
|
||||
|
||||
|
||||
**References:**
|
||||
- [Using On-Demand TLS](https://caddyserver.com/docs/automatic-https#using-on-demand-tls)
|
||||
- [Add option for a backend check to approve use of on-demand TLS](https://github.com/caddyserver/caddy/pull/1939)
|
||||
- [Serving tens of thousands of domains over HTTPS with Caddy](https://caddy.community/t/serving-tens-of-thousands-of-domains-over-https-with-caddy/11179)
|
||||
|
||||
### Cluster operations
|
||||
|
||||
These endpoints have a dedicated OpenAPI spec.
|
||||
- APIv1 - [HTML spec](https://garagehq.deuxfleurs.fr/api/garage-admin-v1.html) - [OpenAPI YAML](https://garagehq.deuxfleurs.fr/api/garage-admin-v1.yml)
|
||||
- APIv0 (deprecated) - [HTML spec](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.html) - [OpenAPI YAML](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.yml)
|
||||
|
||||
Requesting the API from the command line can be as simple as running:
|
||||
|
||||
```bash
|
||||
curl -H 'Authorization: Bearer s3cr3t' http://localhost:3903/v0/status | jq
|
||||
```
|
||||
|
||||
For more advanced use cases, we recommend using a SDK.
|
||||
[Go to the "Build your own app" section to know how to use our SDKs](@/documentation/build/_index.md)
|
||||
|
||||
@@ -13,16 +13,19 @@ consistency_mode = "consistent"
|
||||
|
||||
metadata_dir = "/var/lib/garage/meta"
|
||||
data_dir = "/var/lib/garage/data"
|
||||
metadata_snapshots_dir = "/var/lib/garage/snapshots"
|
||||
metadata_fsync = true
|
||||
data_fsync = false
|
||||
disable_scrub = false
|
||||
use_local_tz = false
|
||||
metadata_auto_snapshot_interval = "6h"
|
||||
|
||||
db_engine = "lmdb"
|
||||
|
||||
block_size = "1M"
|
||||
block_ram_buffer_max = "256MiB"
|
||||
|
||||
block_max_concurrent_reads = 16
|
||||
block_max_concurrent_writes_per_request =10
|
||||
lmdb_map_size = "1T"
|
||||
|
||||
compression_level = 1
|
||||
@@ -31,6 +34,9 @@ rpc_secret = "4425f5c26c5e11581d3223904324dcb5b5d5dfb14e5e7f35e38c595424f5f1e6"
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
rpc_bind_outgoing = false
|
||||
rpc_public_addr = "[fc00:1::1]:3901"
|
||||
# or set rpc_public_adr_subnet to filter down autodiscovery to a subnet:
|
||||
# rpc_public_addr_subnet = "2001:0db8:f00:b00:/64"
|
||||
|
||||
|
||||
allow_world_readable_secrets = false
|
||||
|
||||
@@ -41,20 +47,24 @@ bootstrap_peers = [
|
||||
"212fd62eeaca72c122b45a7f4fa0f55e012aa5e24ac384a72a3016413fa724ff@[fc00:F::1]:3901",
|
||||
]
|
||||
|
||||
allow_punycode = false
|
||||
|
||||
[consul_discovery]
|
||||
api = "catalog"
|
||||
consul_http_addr = "http://127.0.0.1:8500"
|
||||
consul_http_addr = "https://127.0.0.1:8500"
|
||||
tls_skip_verify = false
|
||||
service_name = "garage-daemon"
|
||||
|
||||
ca_cert = "/etc/consul/consul-ca.crt"
|
||||
client_cert = "/etc/consul/consul-client.crt"
|
||||
client_key = "/etc/consul/consul-key.crt"
|
||||
|
||||
# for `agent` API mode, unset client_cert and client_key, and optionally enable `token`
|
||||
# token = "abcdef-01234-56789"
|
||||
tls_skip_verify = false
|
||||
|
||||
tags = [ "dns-enabled" ]
|
||||
meta = { dns-acl = "allow trusted" }
|
||||
|
||||
datacenters = ["dc1", "dc2", "dc3"]
|
||||
|
||||
[kubernetes_discovery]
|
||||
namespace = "garage"
|
||||
@@ -70,10 +80,12 @@ root_domain = ".s3.garage"
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".web.garage"
|
||||
add_host_to_metrics = true
|
||||
|
||||
[admin]
|
||||
api_bind_addr = "0.0.0.0:3903"
|
||||
metrics_token = "BCAdFjoa9G0KJR0WXnHHm7fs1ZAbfpI8iIZ+Z/a2NgI="
|
||||
metrics_require_token = true
|
||||
admin_token = "UkLeGWEvHnXBqnueR3ISEMWpOnm40jH2tM2HnnL/0F4="
|
||||
trace_sink = "http://localhost:4317"
|
||||
```
|
||||
@@ -86,12 +98,16 @@ The following gives details about each available configuration option.
|
||||
|
||||
[Environment variables](#env_variables).
|
||||
|
||||
Top-level configuration options:
|
||||
Top-level configuration options, in alphabetical order:
|
||||
[`allow_punycode`](#allow_punycode),
|
||||
[`allow_world_readable_secrets`](#allow_world_readable_secrets),
|
||||
[`block_max_concurrent_reads`](#block_max_concurrent_reads),
|
||||
[`block_max_concurrent_writes_per_request`](#block_max_concurrent_writes_per_request),
|
||||
[`block_ram_buffer_max`](#block_ram_buffer_max),
|
||||
[`block_size`](#block_size),
|
||||
[`bootstrap_peers`](#bootstrap_peers),
|
||||
[`compression_level`](#compression_level),
|
||||
[`consistency_mode`](#consistency_mode),
|
||||
[`data_dir`](#data_dir),
|
||||
[`data_fsync`](#data_fsync),
|
||||
[`db_engine`](#db_engine),
|
||||
@@ -100,12 +116,14 @@ Top-level configuration options:
|
||||
[`metadata_auto_snapshot_interval`](#metadata_auto_snapshot_interval),
|
||||
[`metadata_dir`](#metadata_dir),
|
||||
[`metadata_fsync`](#metadata_fsync),
|
||||
[`metadata_snapshots_dir`](#metadata_snapshots_dir),
|
||||
[`replication_factor`](#replication_factor),
|
||||
[`consistency_mode`](#consistency_mode),
|
||||
[`rpc_bind_addr`](#rpc_bind_addr),
|
||||
[`rpc_bind_outgoing`](#rpc_bind_outgoing),
|
||||
[`rpc_public_addr`](#rpc_public_addr),
|
||||
[`rpc_secret`/`rpc_secret_file`](#rpc_secret).
|
||||
[`rpc_public_addr_subnet`](#rpc_public_addr_subnet)
|
||||
[`rpc_secret`/`rpc_secret_file`](#rpc_secret),
|
||||
[`use_local_tz`](#use_local_tz).
|
||||
|
||||
The `[consul_discovery]` section:
|
||||
[`api`](#consul_api),
|
||||
@@ -113,12 +131,14 @@ The `[consul_discovery]` section:
|
||||
[`client_cert`](#consul_client_cert_and_key),
|
||||
[`client_key`](#consul_client_cert_and_key),
|
||||
[`consul_http_addr`](#consul_http_addr),
|
||||
[`datacenters`](#consul_datacenters)
|
||||
[`meta`](#consul_tags_and_meta),
|
||||
[`service_name`](#consul_service_name),
|
||||
[`tags`](#consul_tags_and_meta),
|
||||
[`tls_skip_verify`](#consul_tls_skip_verify),
|
||||
[`token`](#consul_token).
|
||||
|
||||
|
||||
The `[kubernetes_discovery]` section:
|
||||
[`namespace`](#kube_namespace),
|
||||
[`service_name`](#kube_service_name),
|
||||
@@ -130,24 +150,30 @@ The `[s3_api]` section:
|
||||
[`s3_region`](#s3_region).
|
||||
|
||||
The `[s3_web]` section:
|
||||
[`add_host_to_metrics`](#web_add_host_to_metrics),
|
||||
[`bind_addr`](#web_bind_addr),
|
||||
[`root_domain`](#web_root_domain).
|
||||
|
||||
The `[admin]` section:
|
||||
[`api_bind_addr`](#admin_api_bind_addr),
|
||||
[`metrics_require_token`](#admin_metrics_require_token),
|
||||
[`metrics_token`/`metrics_token_file`](#admin_metrics_token),
|
||||
[`admin_token`/`admin_token_file`](#admin_token),
|
||||
[`trace_sink`](#admin_trace_sink),
|
||||
|
||||
### Environment variables {#env_variables}
|
||||
|
||||
The following configuration parameter must be specified as an environment
|
||||
variable, it does not exist in the configuration file:
|
||||
The following configuration parameters must be specified as environment variables,
|
||||
they do not exist in the configuration file:
|
||||
|
||||
- `GARAGE_LOG_TO_SYSLOG` (since v0.9.4): set this to `1` or `true` to make the
|
||||
- `GARAGE_LOG_TO_SYSLOG` (since `v0.9.4`): set this to `1` or `true` to make the
|
||||
Garage daemon send its logs to `syslog` (using the libc `syslog` function)
|
||||
instead of printing to stderr.
|
||||
|
||||
- `GARAGE_LOG_TO_JOURNALD` (since `v1.2.0`): set this to `1` or `true` to make the
|
||||
Garage daemon send its logs to `journald` (using the native protocol of `systemd-journald`)
|
||||
instead of printing to stderr.
|
||||
|
||||
The following environment variables can be used to override the corresponding
|
||||
values in the configuration file:
|
||||
|
||||
@@ -159,7 +185,7 @@ values in the configuration file:
|
||||
|
||||
### Top-level configuration options
|
||||
|
||||
#### `replication_factor` {#replication_factor}
|
||||
#### `replication_factor` (since `v1.0.0`) {#replication_factor}
|
||||
|
||||
The replication factor can be any positive integer smaller or equal the node count in your cluster.
|
||||
The chosen replication factor has a big impact on the cluster's failure tolerancy and performance characteristics.
|
||||
@@ -207,7 +233,7 @@ is in progress. In theory, no data should be lost as rebalancing is a
|
||||
routine operation for Garage, although we cannot guarantee you that everything
|
||||
will go right in such an extreme scenario.
|
||||
|
||||
#### `consistency_mode` {#consistency_mode}
|
||||
#### `consistency_mode` (since `v1.0.0`) {#consistency_mode}
|
||||
|
||||
The consistency mode setting determines the read and write behaviour of your cluster.
|
||||
|
||||
@@ -269,6 +295,7 @@ as the index of all objects, object version and object blocks.
|
||||
|
||||
Store this folder on a fast SSD drive if possible to maximize Garage's performance.
|
||||
|
||||
|
||||
#### `data_dir` {#data_dir}
|
||||
|
||||
The directory in which Garage will store the data blocks of objects.
|
||||
@@ -289,14 +316,34 @@ data_dir = [
|
||||
See [the dedicated documentation page](@/documentation/operations/multi-hdd.md)
|
||||
on how to operate Garage in such a setup.
|
||||
|
||||
#### `metadata_snapshots_dir` (since `v1.1.0`) {#metadata_snapshots_dir}
|
||||
|
||||
The directory in which Garage will store metadata snapshots when it
|
||||
performs a snapshot of the metadata database, either when instructed to do
|
||||
so from a RPC call or regularly through
|
||||
[`metadata_auto_snapshot_interval`](#metadata_auto_snapshot_interval).
|
||||
|
||||
By default, Garage will store snapshots into a `snapshots/` subdirectory
|
||||
of [`metadata_dir`](#metadata_dir). This might quickly fill up your
|
||||
metadata storage space if you use snapshots, because Garage will need up
|
||||
to 4x the space of the existing metadata database: each snapshot requires
|
||||
roughly as much space as the original database, and Garage temporarily
|
||||
needs to store up to three different snapshots before it cleans up the oldest
|
||||
snapshot to go back to two stored snapshots.
|
||||
|
||||
To prevent filling your disk, you might to change this setting to a
|
||||
directory with ample available space, e.g. on the same storage space as
|
||||
[`data_dir`](#data_dir).
|
||||
|
||||
#### `db_engine` (since `v0.8.0`) {#db_engine}
|
||||
|
||||
Since `v0.8.0`, Garage can use alternative storage backends as follows:
|
||||
|
||||
| DB engine | `db_engine` value | Database path |
|
||||
| --------- | ----------------- | ------------- |
|
||||
| [LMDB](https://www.lmdb.tech) (since `v0.8.0`, default since `v0.9.0`) | `"lmdb"` | `<metadata_dir>/db.lmdb/` |
|
||||
| [LMDB](https://www.symas.com/lmdb) (since `v0.8.0`, default since `v0.9.0`) | `"lmdb"` | `<metadata_dir>/db.lmdb/` |
|
||||
| [Sqlite](https://sqlite.org) (since `v0.8.0`) | `"sqlite"` | `<metadata_dir>/db.sqlite` |
|
||||
| [Fjall](https://github.com/fjall-rs/fjall) (**experimental support** since `v1.3.0`/`v2.1.0`) | `"fjall"` | `<metadata_dir>/db.fjall/` |
|
||||
| [Sled](https://sled.rs) (old default, removed since `v1.0`) | `"sled"` | `<metadata_dir>/db/` |
|
||||
|
||||
Sled was supported until Garage v0.9.x, and was removed in Garage v1.0.
|
||||
@@ -305,8 +352,16 @@ old Sled metadata databases to another engine.
|
||||
|
||||
Performance characteristics of the different DB engines are as follows:
|
||||
|
||||
- LMDB: the recommended database engine for high-performance distributed clusters.
|
||||
LMDB works very well, but is known to have the following limitations:
|
||||
- **LMDB:** the recommended database engine for high-performance distributed clusters
|
||||
with `replication_factor` ≥ 2.
|
||||
LMDB works well, but is known to have the following limitations:
|
||||
|
||||
- LMDB is prone to database corruption after an unclean shutdown (e.g. a process kill
|
||||
or a power outage). It is recommended to configure
|
||||
[`metadata_auto_snapshot_interval`](#metadata_auto_snapshot_interval) to be
|
||||
able to easily recover from this situation. With `replication_factor` ≥ 2,
|
||||
metadata can also be reconstructed from remote nodes upon corruption
|
||||
(see [Recovering from failures](@/documentation/operations/recovering.md#corrupted_meta)).
|
||||
|
||||
- The data format of LMDB is not portable between architectures, so for
|
||||
instance the Garage database of an x86-64 node cannot be moved to an ARM64
|
||||
@@ -316,22 +371,21 @@ LMDB works very well, but is known to have the following limitations:
|
||||
node to very small database sizes due to how LMDB works; it is therefore
|
||||
not recommended.
|
||||
|
||||
- Several users have reported corrupted LMDB database files after an unclean
|
||||
shutdown (e.g. a power outage). This situation can generally be recovered
|
||||
from if your cluster is geo-replicated (by rebuilding your metadata db from
|
||||
other nodes), or if you have saved regular snapshots at the filesystem
|
||||
level.
|
||||
|
||||
- Keys in LMDB are limited to 511 bytes. This limit translates to limits on
|
||||
object keys in S3 and sort keys in K2V that are limted to 479 bytes.
|
||||
|
||||
- Sqlite: Garage supports Sqlite as an alternative storage backend for
|
||||
metadata, which does not have the issues listed above for LMDB.
|
||||
On versions 0.8.x and earlier, Sqlite should be avoided due to abysmal
|
||||
performance, which was fixed with the addition of `metadata_fsync`.
|
||||
Sqlite is still probably slower than LMDB due to the way we use it,
|
||||
so it is not the best choice for high-performance storage clusters,
|
||||
but it should work fine in many cases.
|
||||
- **Sqlite:** Garage supports Sqlite as an alternative storage backend for
|
||||
metadata, which does not have the issues listed above for LMDB. Sqlite is
|
||||
slower than LMDB, so it is not the best choice for high-performance storage
|
||||
clusters.
|
||||
|
||||
- **Fjall:** a storage engine based on LSM trees, which theoretically allow for
|
||||
higher write throughput than other storage engines that are based on B-trees.
|
||||
Using Fjall could potentially improve Garage's performance significantly in
|
||||
write-heavy workloads. **Support for Fjall is experimental at this point**,
|
||||
we have added it to Garage for evaluation purposes only. **Use it only with
|
||||
test data, and report any issues to our bug tracker. Do not use it for
|
||||
production workloads.**
|
||||
|
||||
It is possible to convert Garage's metadata directory from one format to another
|
||||
using the `garage convert-db` command, which should be used as follows:
|
||||
@@ -370,6 +424,7 @@ Here is how this option impacts the different database engines:
|
||||
|----------|------------------------------------|-------------------------------|
|
||||
| Sqlite | `PRAGMA synchronous = OFF` | `PRAGMA synchronous = NORMAL` |
|
||||
| LMDB | `MDB_NOMETASYNC` + `MDB_NOSYNC` | `MDB_NOMETASYNC` |
|
||||
| Fjall | default options | not supported |
|
||||
|
||||
Note that the Sqlite database is always ran in `WAL` mode (`PRAGMA journal_mode = WAL`).
|
||||
|
||||
@@ -386,10 +441,11 @@ at the cost of a moderate drop in write performance.
|
||||
Similarly to `metatada_fsync`, this is likely not necessary
|
||||
if geographical replication is used.
|
||||
|
||||
#### `metadata_auto_snapshot_interval` (since Garage v0.9.4) {#metadata_auto_snapshot_interval}
|
||||
#### `metadata_auto_snapshot_interval` (since `v0.9.4`) {#metadata_auto_snapshot_interval}
|
||||
|
||||
If this value is set, Garage will automatically take a snapshot of the metadata
|
||||
DB file at a regular interval and save it in the metadata directory.
|
||||
DB file at a regular interval and save it in the metadata directory,
|
||||
or in [`metadata_snapshots_dir`](#metadata_snapshots_dir) if it is set.
|
||||
This parameter can take any duration string that can be parsed by
|
||||
the [`parse_duration`](https://docs.rs/parse_duration/latest/parse_duration/#syntax) crate.
|
||||
|
||||
@@ -398,14 +454,19 @@ corrupted, for instance after an unclean shutdown. See [this
|
||||
page](@/documentation/operations/recovering.md#corrupted_meta) for details.
|
||||
Garage keeps only the two most recent snapshots of the metadata DB and deletes
|
||||
older ones automatically.
|
||||
You can also create metadata snapshots manually at any point using the
|
||||
`garage meta snapshot` command.
|
||||
|
||||
Using snapshots created by Garage is the best option to make snapshots of your
|
||||
node's metadata for potential recovery, as they are guaranteed to be clean and
|
||||
consistent, contrarily to filesystem-level snapshots that may be taken while
|
||||
some writes are in-flight and thus might be corrupted.
|
||||
|
||||
Note that taking a metadata snapshot is a relatively intensive operation as the
|
||||
entire data file is copied. A snapshot being taken might have performance
|
||||
impacts on the Garage node while it is running. If the cluster is under heavy
|
||||
write load when a snapshot operation is running, this might also cause the
|
||||
database file to grow in size significantly as pages cannot be recycled easily.
|
||||
For this reason, it might be better to use filesystem-level snapshots instead
|
||||
if possible.
|
||||
|
||||
#### `disable_scrub` {#disable_scrub}
|
||||
|
||||
@@ -414,7 +475,7 @@ month, with a random delay to avoid all nodes running at the same time. When
|
||||
it scrubs the data directory, Garage will read all of the data files stored on
|
||||
disk to check their integrity, and will rebuild any data files that it finds
|
||||
corrupted, using the remaining valid copies stored on other nodes.
|
||||
See [this page](@/documentation/operations/durability-repair.md#scrub) for details.
|
||||
See [this page](@/documentation/operations/durability-repairs.md#scrub) for details.
|
||||
|
||||
Set the `disable_scrub` configuration value to `true` if you don't need Garage
|
||||
to scrub the data directory, for instance if you are already scrubbing at the
|
||||
@@ -423,6 +484,13 @@ you should delete it from the data directory and then call `garage repair
|
||||
blocks` on the node to ensure that it re-obtains a copy from another node on
|
||||
the network.
|
||||
|
||||
#### `use_local_tz` (since `v1.1.0`) {#use_local_tz}
|
||||
|
||||
By default, Garage runs the lifecycle worker every day at midnight in UTC. Set the
|
||||
`use_local_tz` configuration value to `true` if you want Garage to run the
|
||||
lifecycle worker at midnight in your local timezone. If you have multiple nodes,
|
||||
you should also ensure that each node has the same timezone configuration.
|
||||
|
||||
#### `block_size` {#block_size}
|
||||
|
||||
Garage splits stored objects in consecutive chunks of size `block_size`
|
||||
@@ -438,7 +506,7 @@ files will remain available. This however means that chunks from existing files
|
||||
will not be deduplicated with chunks from newly uploaded files, meaning you
|
||||
might use more storage space that is optimally possible.
|
||||
|
||||
#### `block_ram_buffer_max` (since v0.9.4) {#block_ram_buffer_max}
|
||||
#### `block_ram_buffer_max` (since `v0.9.4`) {#block_ram_buffer_max}
|
||||
|
||||
A limit on the total size of data blocks kept in RAM by S3 API nodes awaiting
|
||||
to be sent to storage nodes asynchronously.
|
||||
@@ -469,6 +537,37 @@ node.
|
||||
|
||||
The default value is 256MiB.
|
||||
|
||||
#### `block_max_concurrent_reads` (since `v1.3.0` / `v2.1.0`) {#block_max_concurrent_reads}
|
||||
|
||||
The maximum number of blocks (individual files in the data directory) open
|
||||
simultaneously for reading.
|
||||
|
||||
Reducing this number does not limit the number of data blocks that can be
|
||||
transferred through the network simultaneously. This mechanism was just added
|
||||
as a backpressure mechanism for HDD read speed: it helps avoid a situation
|
||||
where too many requests are coming in and Garage is reading too many block
|
||||
files simultaneously, thus not making timely progress on any of the reads.
|
||||
|
||||
When a request to read a data block comes in through the network, the requests
|
||||
awaits for one of the `block_max_concurrent_reads` slots to be available
|
||||
(internally implemented using a Semaphore object). Once it acquired a read
|
||||
slot, it reads the entire block file to RAM and frees the slot as soon as the
|
||||
block file is finished reading. Only after the slot is released will the
|
||||
block's data start being transferred over the network. If the request fails to
|
||||
acquire a reading slot wihtin 15 seconds, it fails with a timeout error.
|
||||
Timeout events can be monitored through the `block_read_semaphore_timeouts`
|
||||
metric in Prometheus: a non-zero number of such events indicates an I/O
|
||||
bottleneck on HDD read speed.
|
||||
|
||||
|
||||
#### `block_max_concurrent_writes_per_request` (since `v1.3.1` / `v2.2.0`) {#block_max_concurrent_writes_per_request}
|
||||
|
||||
This parameter is designed to adapt to the concurrent write performance of
|
||||
different storage media. Maximum number of parallel block writes per put request.
|
||||
Higher values may improve throughput but increase memory usage.
|
||||
|
||||
Default value: 3. Recommended values: 10-30 for NVMe, 3-10 for spinning HDD.
|
||||
|
||||
#### `lmdb_map_size` {#lmdb_map_size}
|
||||
|
||||
This parameters can be used to set the map size used by LMDB,
|
||||
@@ -525,7 +624,7 @@ the node, even in the case of a NAT: the NAT should be configured to forward the
|
||||
port number to the same internal port nubmer. This means that if you have several nodes running
|
||||
behind a NAT, they should each use a different RPC port number.
|
||||
|
||||
#### `rpc_bind_outgoing`(since v0.9.2) {#rpc_bind_outgoing}
|
||||
#### `rpc_bind_outgoing` (since `v0.9.2`) {#rpc_bind_outgoing}
|
||||
|
||||
If enabled, pre-bind all sockets for outgoing connections to the same IP address
|
||||
used for listening (the IP address specified in `rpc_bind_addr`) before
|
||||
@@ -543,6 +642,14 @@ RPC calls. **This parameter is optional but recommended.** In case you have
|
||||
a NAT that binds the RPC port to a port that is different on your public IP,
|
||||
this field might help making it work.
|
||||
|
||||
#### `rpc_public_addr_subnet` {#rpc_public_addr_subnet}
|
||||
In case `rpc_public_addr` is not set, but autodiscovery is used, this allows
|
||||
filtering the list of automatically discovered IPs to a specific subnet.
|
||||
|
||||
For example, if nodes should pick *their* IP inside a specific subnet, but you
|
||||
don't want to explicitly write the IP down (as it's dynamic, or you want to
|
||||
share configs across nodes), you can use this option.
|
||||
|
||||
#### `bootstrap_peers` {#bootstrap_peers}
|
||||
|
||||
A list of peer identifiers on which to contact other Garage peers of this cluster.
|
||||
@@ -559,7 +666,7 @@ be obtained by running `garage node id` and then included directly in the
|
||||
key will be returned by `garage node id` and you will have to add the IP
|
||||
yourself.
|
||||
|
||||
### `allow_world_readable_secrets` or `GARAGE_ALLOW_WORLD_READABLE_SECRETS` (env) {#allow_world_readable_secrets}
|
||||
#### `allow_world_readable_secrets` or `GARAGE_ALLOW_WORLD_READABLE_SECRETS` (env) {#allow_world_readable_secrets}
|
||||
|
||||
Garage checks the permissions of your secret files to make sure they're not
|
||||
world-readable. In some cases, the check might fail and consider your files as
|
||||
@@ -571,6 +678,13 @@ permission verification.
|
||||
Alternatively, you can set the `GARAGE_ALLOW_WORLD_READABLE_SECRETS`
|
||||
environment variable to `true` to bypass the permissions check.
|
||||
|
||||
#### `allow_punycode` {#allow_punycode}
|
||||
|
||||
Allow creating buckets with names containing punycode. When used for buckets served
|
||||
as websites, this allows using almost any unicode character in the domain name.
|
||||
|
||||
Default to `false`.
|
||||
|
||||
### The `[consul_discovery]` section
|
||||
|
||||
Garage supports discovering other nodes of the cluster using Consul. For this
|
||||
@@ -626,6 +740,18 @@ node_prefix "" {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### `datacenters` {#consul_datacenters}
|
||||
|
||||
Optional list of datacenters that allow garage to do service discovery when Consul is configured in WAN federation.
|
||||
|
||||
Example: `datacenters = ["dc1", "dc2", "dc3"]`
|
||||
|
||||
In a WAN configuration, by default the Consul services API only responds with
|
||||
local LAN services. When a list of datacenters is specified using this option,
|
||||
Garage will query the consul server API by datacenter directly, allowing for
|
||||
Garage to discover nodes across the Consul WAN.
|
||||
|
||||
#### `tags` and `meta` {#consul_tags_and_meta}
|
||||
|
||||
Additional list of tags and map of service meta to add during service registration.
|
||||
@@ -701,6 +827,13 @@ For instance, if `root_domain` is `web.garage.eu`, a bucket called `deuxfleurs.f
|
||||
will be accessible either with hostname `deuxfleurs.fr.web.garage.eu`
|
||||
or with hostname `deuxfleurs.fr`.
|
||||
|
||||
#### `add_host_to_metrics` {#web_add_host_to_metrics}
|
||||
|
||||
Whether to include the requested domain name (HTTP `Host` header) in the
|
||||
Prometheus metrics of the web endpoint. This is disabled by default as the
|
||||
number of possible values is not bounded and can be a source of cardinality
|
||||
explosion in the exported metrics.
|
||||
|
||||
|
||||
### The `[admin]` section
|
||||
|
||||
@@ -715,10 +848,34 @@ See [administration API reference](@/documentation/reference-manual/admin-api.md
|
||||
Alternatively, since `v0.8.5`, a path can be used to create a unix socket. Note that for security reasons,
|
||||
the socket will have 0220 mode. Make sure to set user and group permissions accordingly.
|
||||
|
||||
#### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
|
||||
|
||||
The token for accessing all administration functions on the admin endpoint,
|
||||
with the exception of the metrics endpoint (see `metrics_token`).
|
||||
|
||||
You can use any random string for this value. We recommend generating a random
|
||||
token with `openssl rand -base64 32`.
|
||||
|
||||
For Garage version earlier than `v2.0`, if this token is not set,
|
||||
access to these endpoints is disabled entirely.
|
||||
|
||||
Since Garage `v2.0`, additional admin API tokens can be defined dynamically
|
||||
in your Garage cluster using administration commands. This new admin token system
|
||||
is more flexible since it allows admin tokens to have an expiration date,
|
||||
and to have a scope restricted to certain admin API functions. If `admin_token`
|
||||
is set, it behaves as an admin token without expiration and with full scope.
|
||||
Otherwise, only admin API tokens defined dynamically can be used.
|
||||
|
||||
`admin_token` was introduced in Garage `v0.7.2`.
|
||||
`admin_token_file` and the `GARAGE_ADMIN_TOKEN` environment variable are supported since Garage `v0.8.2`.
|
||||
|
||||
`GARAGE_ADMIN_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
||||
|
||||
#### `metrics_token`, `metrics_token_file` or `GARAGE_METRICS_TOKEN`, `GARAGE_METRICS_TOKEN_FILE` (env) {#admin_metrics_token}
|
||||
|
||||
The token for accessing the Metrics endpoint. If this token is not set, the
|
||||
Metrics endpoint can be accessed without access control.
|
||||
The token for accessing the Prometheus metrics endpoint (`/metrics`).
|
||||
If this token is not set, and unless `metrics_require_token` is set to `true`,
|
||||
the metrics endpoint can be accessed without access control.
|
||||
|
||||
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
|
||||
|
||||
@@ -727,17 +884,12 @@ You can use any random string for this value. We recommend generating a random t
|
||||
|
||||
`GARAGE_METRICS_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
||||
|
||||
#### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
|
||||
#### `metrics_require_token` (since `v2.0.0`) {#admin_metrics_require_token}
|
||||
|
||||
The token for accessing all of the other administration endpoints. If this
|
||||
token is not set, access to these endpoints is disabled entirely.
|
||||
|
||||
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
|
||||
|
||||
`admin_token` was introduced in Garage `v0.7.2`.
|
||||
`admin_token_file` and the `GARAGE_ADMIN_TOKEN` environment variable are supported since Garage `v0.8.2`.
|
||||
|
||||
`GARAGE_ADMIN_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
||||
If this is set to `true`, accessing the metrics endpoint will always require
|
||||
an access token. Valid tokens include the `metrics_token` if it is set,
|
||||
and admin API token defined dynamicaly in Garage which have
|
||||
the `Metrics` endpoint in their scope.
|
||||
|
||||
#### `trace_sink` {#admin_trace_sink}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ directed to a Garage cluster can be handled independently of one another instead
|
||||
of going through a central bottleneck (the leader node).
|
||||
As a consequence, requests can be handled much faster, even in cases where latency
|
||||
between cluster nodes is important (see our [benchmarks](@/documentation/design/benchmarks/index.md) for data on this).
|
||||
This is particularly usefull when nodes are far from one another and talk to one other through standard Internet connections.
|
||||
This is particularly useful when nodes are far from one another and talk to one other through standard Internet connections.
|
||||
|
||||
### Web server for static websites
|
||||
|
||||
@@ -129,5 +129,5 @@ related to objects stored in an S3 bucket.
|
||||
In the context of our research project, [Aérogramme](https://aerogramme.deuxfleurs.fr),
|
||||
K2V is used to provide metadata and log storage for operations on encrypted e-mail storage.
|
||||
|
||||
Learn more on the specification of K2V [here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/k2v/doc/drafts/k2v-spec.md)
|
||||
Learn more on the specification of K2V [here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/f8be15c37db857e177d543de7be863692628d567/doc/drafts/k2v-spec.md)
|
||||
and on how to enable it in Garage [here](@/documentation/reference-manual/k2v.md).
|
||||
|
||||
@@ -16,7 +16,7 @@ the `k2v` feature flag enabled can be obtained from our download page under
|
||||
with `-k2v` (example: `v0.7.2-k2v`).
|
||||
|
||||
The specification of the K2V API can be found
|
||||
[here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/doc/drafts/k2v-spec.md).
|
||||
[here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/f8be15c37db857e177d543de7be863692628d567/doc/drafts/k2v-spec.md).
|
||||
This document also includes a high-level overview of K2V's design.
|
||||
|
||||
The K2V API uses AWSv4 signatures for authentification, same as the S3 API.
|
||||
|
||||
@@ -392,7 +392,7 @@ 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
|
||||
Number of data items sent to/received from other nodes during resync procedures
|
||||
|
||||
```
|
||||
table_sync_items_received{from="<remote node>",table_name="bucket_v2"} 3
|
||||
|
||||
@@ -23,17 +23,17 @@ Feel free to open a PR to suggest fixes this table. Minio is missing because the
|
||||
- 2022-05-25 - Many Ceph S3 endpoints are not documented but implemented. Following a notification from the Ceph community, we added them.
|
||||
|
||||
|
||||
|
||||
## High-level features
|
||||
|
||||
| Feature | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) |
|
||||
|------------------------------|----------------------------------|-----------------|---------------|---------|-----|
|
||||
| [signature v2](https://docs.aws.amazon.com/general/latest/gr/signature-version-2.html) (deprecated) | ❌ Missing | ✅ | ✅ | ✅ | ✅ |
|
||||
| [signature v2](https://docs.aws.amazon.com/AmazonS3/latest/API/Appendix-Sigv2.html) (deprecated) | ❌ Missing | ✅ | ✅ | ✅ | ✅ |
|
||||
| [signature v4](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) | ✅ Implemented | ✅ | ✅ | ❌ | ✅ |
|
||||
| [URL path-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#path-style-access) (eg. `host.tld/bucket/key`) | ✅ Implemented | ✅ | ✅ | ❓| ✅ |
|
||||
| [URL vhost-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access) URL (eg. `bucket.host.tld/key`) | ✅ Implemented | ❌| ✅| ✅ | ✅ |
|
||||
| [Presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) | ✅ Implemented | ❌| ✅ | ✅ | ✅(❓) |
|
||||
| [SSE-C encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html) | ✅ Implemented | ❓ | ✅ | ❌ | ✅ |
|
||||
| [Bucket versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html) | ❌ Missing | ✅ | ✅ | ❌ | ✅ |
|
||||
|
||||
*Note:* OpenIO does not says if it supports presigned URLs. Because it is part
|
||||
of signature v4 and they claim they support it without additional precisions,
|
||||
|
||||
@@ -42,7 +42,7 @@ The general principle are similar, but details have not been updated.**
|
||||
A version is defined by the existence of at least one entry in the blocks table for a certain version UUID.
|
||||
We must keep the following invariant: if a version exists in the blocks table, it has to be referenced in the objects table.
|
||||
We explicitly manage concurrent versions of an object: the version timestamp and version UUID columns are index columns, thus we may have several concurrent versions of an object.
|
||||
Important: before deleting an older version from the objects table, we must make sure that we did a successfull delete of the blocks of that version from the blocks table.
|
||||
Important: before deleting an older version from the objects table, we must make sure that we did a successful delete of the blocks of that version from the blocks table.
|
||||
|
||||
Thus, the workflow for reading an object is as follows:
|
||||
|
||||
@@ -95,7 +95,7 @@ Known issue: if someone is reading from a version that we want to delete and the
|
||||
Usefull metadata:
|
||||
|
||||
- list of versions that reference this block in the Casandra table, so that we can do GC by checking in Cassandra that the lines still exist
|
||||
- list of other nodes that we know have acknowledged a write of this block, usefull in the rebalancing algorithm
|
||||
- list of other nodes that we know have acknowledged a write of this block, useful in the rebalancing algorithm
|
||||
|
||||
Write strategy: have a single thread that does all write IO so that it is serialized (or have several threads that manage independent parts of the hash space). When writing a blob, write it to a temporary file, close, then rename so that a concurrent read gets a consistent result (either not found or found with whole content).
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Migrating from 0.3 to 0.4"
|
||||
weight = 20
|
||||
weight = 80
|
||||
+++
|
||||
|
||||
**Migrating from 0.3 to 0.4 is unsupported. This document is only intended to
|
||||
@@ -68,7 +68,7 @@ The migration steps are as follows:
|
||||
5. Turn off Garage 0.3
|
||||
|
||||
6. Backup metadata folders if you can (i.e. if you have space to do it
|
||||
somewhere). Backuping data folders could also be usefull but that's much
|
||||
somewhere). Backuping data folders could also be useful but that's much
|
||||
harder to do. If your filesystem supports snapshots, this could be a good
|
||||
time to use them.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Migrating from 0.5 to 0.6"
|
||||
weight = 15
|
||||
weight = 75
|
||||
+++
|
||||
|
||||
**This guide explains how to migrate to 0.6 if you have an existing 0.5 cluster.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Migrating from 0.6 to 0.7"
|
||||
weight = 14
|
||||
weight = 74
|
||||
+++
|
||||
**This guide explains how to migrate to 0.7 if you have an existing 0.6 cluster.
|
||||
We don't recommend trying to migrate to 0.7 directly from 0.5 or older.**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Migrating from 0.7 to 0.8"
|
||||
weight = 13
|
||||
weight = 73
|
||||
+++
|
||||
|
||||
**This guide explains how to migrate to 0.8 if you have an existing 0.7 cluster.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Migrating from 0.8 to 0.9"
|
||||
weight = 12
|
||||
weight = 72
|
||||
+++
|
||||
|
||||
**This guide explains how to migrate to 0.9 if you have an existing 0.8 cluster.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Migrating from 0.9 to 1.0"
|
||||
weight = 11
|
||||
weight = 71
|
||||
+++
|
||||
|
||||
**This guide explains how to migrate to 1.0 if you have an existing 0.9 cluster.
|
||||
@@ -19,15 +19,15 @@ The following are **breaking changes** in Garage v1.0 that require your attentio
|
||||
- The Sled metadata db engine has been **removed**. If your cluster was still
|
||||
using Sled, you will need to **use a Garage v0.9.x binary** to convert the
|
||||
database using the `garage convert-db` subcommand. See
|
||||
[here](@/documentation/reference-manual/configuration/#db_engine) for the
|
||||
[here](@/documentation/reference-manual/configuration.md#db_engine) for the
|
||||
details of the procedure.
|
||||
|
||||
The following syntax changes have been made to the configuration file:
|
||||
|
||||
- The `replication_mode` parameter has been split into two parameters:
|
||||
[`replication_factor`](@/documentation/reference-manual/configuration/#replication_factor)
|
||||
[`replication_factor`](@/documentation/reference-manual/configuration.md#replication_factor)
|
||||
and
|
||||
[`consistency_mode`](@/documentation/reference-manual/configuration/#consistency_mode).
|
||||
[`consistency_mode`](@/documentation/reference-manual/configuration.md#consistency_mode).
|
||||
The old syntax using `replication_mode` is still supported for legacy
|
||||
reasons and can still be used.
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
+++
|
||||
title = "Migrating from 1.0 to 2.0"
|
||||
weight = 70
|
||||
+++
|
||||
|
||||
**This guide explains how to migrate to v2.x if you have an existing v1.x.x cluster.
|
||||
We don't recommend trying to migrate to v2.x directly from v0.9.x or older.**
|
||||
|
||||
This migration procedure has been tested on several clusters without issues.
|
||||
However, it is still a *critical procedure* that might cause issues.
|
||||
**Make sure to back up all your data before attempting it!**
|
||||
|
||||
You might also want to read our [general documentation on upgrading Garage](@/documentation/operations/upgrading.md).
|
||||
|
||||
## Changes introduced in v2.0
|
||||
|
||||
The following are **breaking changes** in Garage v2.0 that require your attention when migrating:
|
||||
|
||||
- The administration API has been completely reworked.
|
||||
Some calls to the `/v1/` endpoints will still work but most will not.
|
||||
New endpoints are prefixed by `/v2/`. **You will need to update all your code that makes use of the admin API.**
|
||||
|
||||
- `replication_mode` is no longer a supported configuration parameter,
|
||||
please use `replication_factor` and `consistency_mode` instead.
|
||||
|
||||
## Migration procedure
|
||||
|
||||
The migration to Garage v2.0 can be done with almost no downtime,
|
||||
by restarting all nodes at once in the new version.
|
||||
|
||||
The migration steps are as follows:
|
||||
|
||||
1. Do a `garage repair --all-nodes --yes tables`, check the logs and check that
|
||||
all data seems to be synced correctly between nodes. If you have time, do
|
||||
additional `garage repair` procedures (`blocks`, `versions`, `block_refs`,
|
||||
etc.)
|
||||
|
||||
2. Ensure you have a snapshot of your Garage installation that you can restore
|
||||
to in case the upgrade goes wrong, with one of the following options:
|
||||
|
||||
- You may use the `garage meta snapshot --all` command
|
||||
to make a backup snapshot of the metadata directories of your nodes
|
||||
for backup purposes. Once this command has completed, copy the following
|
||||
files and directories from the `metadata_dir` of all your nodes
|
||||
to somewhere safe: `snapshots`, `cluster_layout`, `data_layout`,
|
||||
`node_key`, `node_key.pub`. (If you have set the `metadata_snapshots_dir`
|
||||
to a different value in your config file, back up that directory instead.)
|
||||
|
||||
- If you are running a filesystem such as ZFS or BTRFS that support
|
||||
snapshotting, you can create a filesystem-level snapshot of the `metadata_dir`
|
||||
of all your nodes to be used as a restoration point if needed.
|
||||
|
||||
- You may also make a back-up manually: turn off each node
|
||||
individually; back up its metadata folder (for instance, use the following
|
||||
command if your metadata directory is `/var/lib/garage/meta`: `cd
|
||||
/var/lib/garage ; tar -acf meta-v1.0.tar.zst meta/`); turn it back on
|
||||
again. This will allow you to take a backup of all nodes without
|
||||
impacting global cluster availability. You can do all nodes of a single
|
||||
zone at once as this does not impact the availability of Garage.
|
||||
|
||||
3. Prepare your updated binaries and configuration files for Garage v2.0.
|
||||
**Remember to update your configuration file to remove `replication_mode` and replace it by `replication_factor`.**
|
||||
|
||||
4. Shut down all v1.0 nodes simultaneously, and restart them all simultaneously
|
||||
in v2.0. Use your favorite deployment tool (Ansible, Kubernetes, Nomad) to
|
||||
achieve this as fast as possible. Garage v2.0 should be in a working state
|
||||
as soon as enough nodes have started.
|
||||
|
||||
5. Monitor your cluster in the following hours to see if it works well under
|
||||
your production load.
|
||||
@@ -1,6 +1,6 @@
|
||||
+++
|
||||
title = "Testing strategy"
|
||||
weight = 30
|
||||
weight = 100
|
||||
+++
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ There are two reasons for this:
|
||||
|
||||
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.
|
||||
Never do anything just because it looks nice or because we "think" it might be useful 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
|
||||
|
||||
@@ -13,8 +13,12 @@ We will bump the version numbers prefixed to each API endpoint each time the syn
|
||||
or semantics change, meaning that code that relies on these endpoints will break
|
||||
when changes are introduced.
|
||||
|
||||
The Garage administration API was introduced in version 0.7.2, this document
|
||||
does not apply to older versions of Garage.
|
||||
The Garage administration API was introduced in version 0.7.2, and was
|
||||
changed several times.
|
||||
|
||||
**THIS DOCUMENT IS DEPRECATED.** We now have an OpenAPI spec which is automatically generated
|
||||
from Garage's source code and is always up-to-date. See `doc/api/garage-admin-v2.html`.
|
||||
Text in this document is no longer kept in sync with the admin API's actual behavior.
|
||||
|
||||
|
||||
## Access control
|
||||
@@ -52,34 +56,28 @@ Returns an HTTP status 200 if the node is ready to answer user's requests,
|
||||
and an HTTP status 503 (Service Unavailable) if there are some partitions
|
||||
for which a quorum of nodes is not available.
|
||||
A simple textual message is also returned in a body with content-type `text/plain`.
|
||||
See `/v1/health` for an API that also returns JSON output.
|
||||
See `/v2/GetClusterHealth` for an API that also returns JSON output.
|
||||
|
||||
### Other special endpoints
|
||||
|
||||
#### CheckDomain `GET /check?domain=<domain>`
|
||||
|
||||
Checks whether this Garage cluster serves a website for domain `<domain>`.
|
||||
Returns HTTP 200 Ok if yes, or HTTP 4xx if no website is available for this domain.
|
||||
|
||||
### Cluster operations
|
||||
|
||||
#### GetClusterStatus `GET /v1/status`
|
||||
#### GetClusterStatus `GET /v2/GetClusterStatus`
|
||||
|
||||
Returns the cluster's current status in JSON, including:
|
||||
|
||||
- ID of the node being queried and its version of the Garage daemon
|
||||
- Live nodes
|
||||
- Currently configured cluster layout
|
||||
- Staged changes to the cluster layout
|
||||
|
||||
Example response body:
|
||||
|
||||
```json
|
||||
{
|
||||
"node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df",
|
||||
"garageVersion": "v1.0.0",
|
||||
"garageFeatures": [
|
||||
"k2v",
|
||||
"lmdb",
|
||||
"sqlite",
|
||||
"metrics",
|
||||
"bundled-libs"
|
||||
],
|
||||
"rustVersion": "1.68.0",
|
||||
"dbEngine": "LMDB (using Heed crate)",
|
||||
"layoutVersion": 5,
|
||||
"nodes": [
|
||||
{
|
||||
@@ -169,7 +167,7 @@ Example response body:
|
||||
}
|
||||
```
|
||||
|
||||
#### GetClusterHealth `GET /v1/health`
|
||||
#### GetClusterHealth `GET /v2/GetClusterHealth`
|
||||
|
||||
Returns the cluster's current health in JSON format, with the following variables:
|
||||
|
||||
@@ -202,7 +200,7 @@ Example response body:
|
||||
}
|
||||
```
|
||||
|
||||
#### ConnectClusterNodes `POST /v1/connect`
|
||||
#### ConnectClusterNodes `POST /v2/ConnectClusterNodes`
|
||||
|
||||
Instructs this Garage node to connect to other Garage nodes at specified addresses.
|
||||
|
||||
@@ -232,7 +230,7 @@ Example response:
|
||||
]
|
||||
```
|
||||
|
||||
#### GetClusterLayout `GET /v1/layout`
|
||||
#### GetClusterLayout `GET /v2/GetClusterLayout`
|
||||
|
||||
Returns the cluster's current layout in JSON, including:
|
||||
|
||||
@@ -293,7 +291,7 @@ Example response body:
|
||||
}
|
||||
```
|
||||
|
||||
#### UpdateClusterLayout `POST /v1/layout`
|
||||
#### UpdateClusterLayout `POST /v2/UpdateClusterLayout`
|
||||
|
||||
Send modifications to the cluster layout. These modifications will
|
||||
be included in the staged role changes, visible in subsequent calls
|
||||
@@ -330,7 +328,7 @@ This returns the new cluster layout with the proposed staged changes,
|
||||
as returned by GetClusterLayout.
|
||||
|
||||
|
||||
#### ApplyClusterLayout `POST /v1/layout/apply`
|
||||
#### ApplyClusterLayout `POST /v2/ApplyClusterLayout`
|
||||
|
||||
Applies to the cluster the layout changes currently registered as
|
||||
staged layout changes.
|
||||
@@ -350,23 +348,11 @@ existing layout in the cluster.
|
||||
This returns the message describing all the calculations done to compute the new
|
||||
layout, as well as the description of the layout as returned by GetClusterLayout.
|
||||
|
||||
#### RevertClusterLayout `POST /v1/layout/revert`
|
||||
#### RevertClusterLayout `POST /v2/RevertClusterLayout`
|
||||
|
||||
Clears all of the staged layout changes.
|
||||
|
||||
Request body format:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 13
|
||||
}
|
||||
```
|
||||
|
||||
Reverting the staged changes is done by incrementing the version number
|
||||
and clearing the contents of the staged change list.
|
||||
Similarly to the CLI, the body must include the incremented
|
||||
version number, which MUST be 1 + the value of the currently
|
||||
existing layout in the cluster.
|
||||
This requests contains an empty body.
|
||||
|
||||
This returns the new cluster layout with all changes reverted,
|
||||
as returned by GetClusterLayout.
|
||||
@@ -374,7 +360,7 @@ as returned by GetClusterLayout.
|
||||
|
||||
### Access key operations
|
||||
|
||||
#### ListKeys `GET /v1/key`
|
||||
#### ListKeys `GET /v2/ListKeys`
|
||||
|
||||
Returns all API access keys in the cluster.
|
||||
|
||||
@@ -393,8 +379,8 @@ Example response:
|
||||
]
|
||||
```
|
||||
|
||||
#### GetKeyInfo `GET /v1/key?id=<acces key id>`
|
||||
#### GetKeyInfo `GET /v1/key?search=<pattern>`
|
||||
#### GetKeyInfo `GET /v2/GetKeyInfo?id=<acces key id>`
|
||||
#### GetKeyInfo `GET /v2/GetKeyInfo?search=<pattern>`
|
||||
|
||||
Returns information about the requested API access key.
|
||||
|
||||
@@ -468,7 +454,7 @@ Example response:
|
||||
}
|
||||
```
|
||||
|
||||
#### CreateKey `POST /v1/key`
|
||||
#### CreateKey `POST /v2/CreateKey`
|
||||
|
||||
Creates a new API access key.
|
||||
|
||||
@@ -483,7 +469,7 @@ Request body format:
|
||||
This returns the key info, including the created secret key,
|
||||
in the same format as the result of GetKeyInfo.
|
||||
|
||||
#### ImportKey `POST /v1/key/import`
|
||||
#### ImportKey `POST /v2/ImportKey`
|
||||
|
||||
Imports an existing API key.
|
||||
This will check that the imported key is in the valid format, i.e.
|
||||
@@ -501,7 +487,7 @@ Request body format:
|
||||
|
||||
This returns the key info in the same format as the result of GetKeyInfo.
|
||||
|
||||
#### UpdateKey `POST /v1/key?id=<acces key id>`
|
||||
#### UpdateKey `POST /v2/UpdateKey?id=<acces key id>`
|
||||
|
||||
Updates information about the specified API access key.
|
||||
|
||||
@@ -523,14 +509,14 @@ The possible flags in `allow` and `deny` are: `createBucket`.
|
||||
|
||||
This returns the key info in the same format as the result of GetKeyInfo.
|
||||
|
||||
#### DeleteKey `DELETE /v1/key?id=<acces key id>`
|
||||
#### DeleteKey `POST /v2/DeleteKey?id=<acces key id>`
|
||||
|
||||
Deletes an API access key.
|
||||
|
||||
|
||||
### Bucket operations
|
||||
|
||||
#### ListBuckets `GET /v1/bucket`
|
||||
#### ListBuckets `GET /v2/ListBuckets`
|
||||
|
||||
Returns all storage buckets in the cluster.
|
||||
|
||||
@@ -572,8 +558,8 @@ Example response:
|
||||
]
|
||||
```
|
||||
|
||||
#### GetBucketInfo `GET /v1/bucket?id=<bucket id>`
|
||||
#### GetBucketInfo `GET /v1/bucket?globalAlias=<alias>`
|
||||
#### GetBucketInfo `GET /v2/GetBucketInfo?id=<bucket id>`
|
||||
#### GetBucketInfo `GET /v2/GetBucketInfo?globalAlias=<alias>`
|
||||
|
||||
Returns information about the requested storage bucket.
|
||||
|
||||
@@ -616,7 +602,7 @@ Example response:
|
||||
}
|
||||
```
|
||||
|
||||
#### CreateBucket `POST /v1/bucket`
|
||||
#### CreateBucket `POST /v2/CreateBucket`
|
||||
|
||||
Creates a new storage bucket.
|
||||
|
||||
@@ -656,7 +642,7 @@ or no alias at all.
|
||||
Technically, you can also specify both `globalAlias` and `localAlias` and that would create
|
||||
two aliases, but I don't see why you would want to do that.
|
||||
|
||||
#### UpdateBucket `PUT /v1/bucket?id=<bucket id>`
|
||||
#### UpdateBucket `POST /v2/UpdateBucket?id=<bucket id>`
|
||||
|
||||
Updates configuration of the given bucket.
|
||||
|
||||
@@ -688,16 +674,38 @@ In `quotas`: new values of `maxSize` and `maxObjects` must both be specified, or
|
||||
to remove the quotas. An absent value will be considered the same as a `null`. It is not possible
|
||||
to change only one of the two quotas.
|
||||
|
||||
#### DeleteBucket `DELETE /v1/bucket?id=<bucket id>`
|
||||
#### DeleteBucket `POST /v2/DeleteBucket?id=<bucket id>`
|
||||
|
||||
Deletes a storage bucket. A bucket cannot be deleted if it is not empty.
|
||||
|
||||
Warning: this will delete all aliases associated with the bucket!
|
||||
|
||||
#### CleanupIncompleteUploads `POST /v2/CleanupIncompleteUploads`
|
||||
|
||||
Cleanup all incomplete uploads in a bucket that are older than a specified number
|
||||
of seconds.
|
||||
|
||||
Request body format:
|
||||
|
||||
```json
|
||||
{
|
||||
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||
"olderThanSecs": 3600
|
||||
}
|
||||
```
|
||||
|
||||
Response format
|
||||
|
||||
```json
|
||||
{
|
||||
"uploadsDeleted": 12
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Operations on permissions for keys on buckets
|
||||
|
||||
#### BucketAllowKey `POST /v1/bucket/allow`
|
||||
#### AllowBucketKey `POST /v2/AllowBucketKey`
|
||||
|
||||
Allows a key to do read/write/owner operations on a bucket.
|
||||
|
||||
@@ -718,7 +726,7 @@ Request body format:
|
||||
Flags in `permissions` which have the value `true` will be activated.
|
||||
Other flags will remain unchanged.
|
||||
|
||||
#### BucketDenyKey `POST /v1/bucket/deny`
|
||||
#### DenyBucketKey `POST /v2/DenyBucketKey`
|
||||
|
||||
Denies a key from doing read/write/owner operations on a bucket.
|
||||
|
||||
@@ -742,19 +750,35 @@ Other flags will remain unchanged.
|
||||
|
||||
### Operations on bucket aliases
|
||||
|
||||
#### GlobalAliasBucket `PUT /v1/bucket/alias/global?id=<bucket id>&alias=<global alias>`
|
||||
#### AddBucketAlias `POST /v2/AddBucketAlias`
|
||||
|
||||
Empty body. Creates a global alias for a bucket.
|
||||
Creates an alias for a bucket in the namespace of a specific access key.
|
||||
To create a global alias, specify the `globalAlias` field.
|
||||
To create a local alias, specify the `localAlias` and `accessKeyId` fields.
|
||||
|
||||
#### GlobalUnaliasBucket `DELETE /v1/bucket/alias/global?id=<bucket id>&alias=<global alias>`
|
||||
Request body format:
|
||||
|
||||
Removes a global alias for a bucket.
|
||||
```json
|
||||
{
|
||||
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||
"globalAlias": "my-bucket"
|
||||
}
|
||||
```
|
||||
|
||||
#### LocalAliasBucket `PUT /v1/bucket/alias/local?id=<bucket id>&accessKeyId=<access key ID>&alias=<local alias>`
|
||||
or:
|
||||
|
||||
Empty body. Creates a local alias for a bucket in the namespace of a specific access key.
|
||||
```json
|
||||
{
|
||||
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
|
||||
"accessKeyId": "GK31c2f218a2e44f485b94239e",
|
||||
"localAlias": "my-bucket"
|
||||
}
|
||||
```
|
||||
|
||||
#### LocalUnaliasBucket `DELETE /v1/bucket/alias/local?id=<bucket id>&accessKeyId<access key ID>&alias=<local alias>`
|
||||
#### RemoveBucketAlias `POST /v2/RemoveBucketAlias`
|
||||
|
||||
Removes a local alias for a bucket in the namespace of a specific access key.
|
||||
Removes an alias for a bucket in the namespace of a specific access key.
|
||||
To remove a global alias, specify the `globalAlias` field.
|
||||
To remove a local alias, specify the `localAlias` and `accessKeyId` fields.
|
||||
|
||||
Request body format: same as AddBucketAlias.
|
||||
|
||||
@@ -562,7 +562,7 @@ token>", v: ["<value1>", ...] }`, with the following fields:
|
||||
- in case of concurrent update and deletion, a `null` is added to the list of concurrent values
|
||||
|
||||
- if the `tombstones` query parameter is set to `true`, tombstones are returned
|
||||
for items that have been deleted (this can be usefull for inserting after an
|
||||
for items that have been deleted (this can be useful for inserting after an
|
||||
item that has been deleted, so that the insert is not considered
|
||||
concurrent with the delete). Tombstones are returned as tuples in the
|
||||
same format with only `null` values
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
*
|
||||
|
||||
!*.txt
|
||||
!*.md
|
||||
|
||||
!assets
|
||||
|
||||
!.gitignore
|
||||
!*.svg
|
||||
!*.png
|
||||
!*.jpg
|
||||
!*.tex
|
||||
!Makefile
|
||||
!.gitignore
|
||||
!assets/*.drawio.pdf
|
||||
|
||||
!talk.pdf
|
||||
@@ -0,0 +1,19 @@
|
||||
ASSETS=../assets/lattice/lattice1.pdf_tex \
|
||||
../assets/lattice/lattice2.pdf_tex \
|
||||
../assets/lattice/lattice3.pdf_tex \
|
||||
../assets/lattice/lattice4.pdf_tex \
|
||||
../assets/lattice/lattice5.pdf_tex \
|
||||
../assets/lattice/lattice6.pdf_tex \
|
||||
../assets/lattice/lattice7.pdf_tex \
|
||||
../assets/lattice/lattice8.pdf_tex \
|
||||
../assets/logos/deuxfleurs.pdf \
|
||||
../assets/timeline-22-24.pdf
|
||||
|
||||
talk.pdf: talk.tex $(ASSETS)
|
||||
pdflatex talk.tex
|
||||
|
||||
%.pdf: %.svg
|
||||
inkscape -D -z --file=$^ --export-pdf=$@
|
||||
|
||||
%.pdf_tex: %.svg
|
||||
inkscape -D -z --file=$^ --export-pdf=$@ --export-latex
|
||||
@@ -0,0 +1,702 @@
|
||||
\nonstopmode
|
||||
\documentclass[aspectratio=169,xcolor={svgnames}]{beamer}
|
||||
\usepackage[utf8]{inputenc}
|
||||
% \usepackage[frenchb]{babel}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{mathtools}
|
||||
\usepackage{breqn}
|
||||
\usepackage{multirow}
|
||||
\usetheme{boxes}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{import}
|
||||
\usepackage{adjustbox}
|
||||
\usepackage[absolute,overlay]{textpos}
|
||||
%\useoutertheme[footline=authortitle,subsection=false]{miniframes}
|
||||
%\useoutertheme[footline=authorinstitute,subsection=false]{miniframes}
|
||||
\useoutertheme{infolines}
|
||||
\setbeamertemplate{headline}{}
|
||||
|
||||
\beamertemplatenavigationsymbolsempty
|
||||
|
||||
\definecolor{TitleOrange}{RGB}{255,137,0}
|
||||
\setbeamercolor{title}{fg=TitleOrange}
|
||||
\setbeamercolor{frametitle}{fg=TitleOrange}
|
||||
|
||||
\definecolor{ListOrange}{RGB}{255,145,5}
|
||||
\setbeamertemplate{itemize item}{\color{ListOrange}$\blacktriangleright$}
|
||||
|
||||
\definecolor{verygrey}{RGB}{70,70,70}
|
||||
\setbeamercolor{normal text}{fg=verygrey}
|
||||
|
||||
|
||||
\usepackage{tabu}
|
||||
\usepackage{multicol}
|
||||
\usepackage{vwcol}
|
||||
\usepackage{stmaryrd}
|
||||
\usepackage{graphicx}
|
||||
|
||||
\usepackage[normalem]{ulem}
|
||||
|
||||
\AtBeginSection[]{
|
||||
\begin{frame}
|
||||
\vfill
|
||||
\centering
|
||||
\begin{beamercolorbox}[sep=8pt,center,shadow=true,rounded=true]{title}
|
||||
\usebeamerfont{title}\insertsectionhead\par%
|
||||
\end{beamercolorbox}
|
||||
\vfill
|
||||
\end{frame}
|
||||
}
|
||||
|
||||
\title{Garage, an S3 backend as reliable as possible}
|
||||
\author{Garage Authors}
|
||||
\date{JoSy S3, 2025-10-08}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\begin{frame}
|
||||
\centering
|
||||
\includegraphics[width=.3\linewidth]{../../sticker/Garage.png}
|
||||
\vspace{1em}
|
||||
|
||||
{\large\bf Garage, an S3 backend as reliable as possible}
|
||||
\vspace{1em}
|
||||
|
||||
\url{https://garagehq.deuxfleurs.fr/}\\
|
||||
\url{mailto:garagehq@deuxfleurs.fr}\\
|
||||
\texttt{\#garage:deuxfleurs.fr} on Matrix
|
||||
\end{frame}
|
||||
|
||||
|
||||
\section{Meet Garage}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{A non-profit initiative}
|
||||
|
||||
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.5\linewidth, valign=t]{../assets/logos/deuxfleurs.pdf}
|
||||
\end{column}
|
||||
\begin{column}{.8\textwidth}
|
||||
\textbf{Part of a degrowth initiative}\\
|
||||
Garage has been created at Deuxfleurs where we experiment running Internet services without datacenter on commodity and refurbished hardware.
|
||||
\end{column}
|
||||
|
||||
\end{columns}
|
||||
\vspace{2em}
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.5\linewidth, valign=t]{../assets/community.png}
|
||||
\end{column}
|
||||
\begin{column}{.8\textwidth}
|
||||
\textbf{Developed by a community}\\
|
||||
{\small Some recent contributors: Arthur C, Charles H, dongdigua, Etienne L, Jonah A, Julien K, Lapineige, MagicRR, Milas B, Niklas M, RockWolf, Schwitzd, trinity-1686a, Xavier S, babykart, Baptiste J, eddster2309, James O'C, Joker9944, Maximilien R, Renjaya RZ, Yureka...}
|
||||
\end{column}
|
||||
|
||||
\end{columns}
|
||||
\vspace{2em}
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.5\linewidth, valign=t]{../assets/logos/AGPLv3_Logo.png}
|
||||
\end{column}
|
||||
\begin{column}{.8\textwidth}
|
||||
\textbf{Owned by nobody, open-core is impossible, zero VC money}\\
|
||||
AGPL + no Contributor License Agreement = Garage ownership spreads among hundredth of contributors.
|
||||
\end{column}
|
||||
|
||||
\end{columns}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Getting support for Garage}
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.4\linewidth, valign=t]{../assets/alex.jpg}
|
||||
\end{column}
|
||||
\begin{column}{.4\textwidth}
|
||||
\textbf{Alex Auvolat}\\
|
||||
PhD; co-founder of Deuxfleurs\\
|
||||
Garage maintainer, Freelance
|
||||
\end{column}
|
||||
\begin{column}{.3\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.4\linewidth, valign=t]{../assets/support.png}
|
||||
\end{column}
|
||||
\begin{column}{.1\textwidth}
|
||||
~
|
||||
\end{column}
|
||||
\end{columns}
|
||||
\vspace{2em}
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.4\linewidth, valign=t]{../assets/quentin.jpg}
|
||||
\end{column}
|
||||
\begin{column}{.4\textwidth}
|
||||
\textbf{Quentin Dufour}\\
|
||||
PhD; co-founder of Deuxfleurs\\
|
||||
Garage contributor, Freelance
|
||||
\end{column}
|
||||
\begin{column}{.4\textwidth}
|
||||
For support requests, write at: \\
|
||||
\url{garagehq@deuxfleurs.fr}
|
||||
\end{column}
|
||||
\end{columns}
|
||||
\vspace{2em}
|
||||
\begin{columns}[t]
|
||||
\begin{column}{.2\textwidth}
|
||||
\centering
|
||||
\adjincludegraphics[width=.4\linewidth, valign=t]{../assets/armael.jpg}
|
||||
\end{column}
|
||||
\begin{column}{.4\textwidth}
|
||||
\textbf{Armaël Guéneau}\\
|
||||
PhD; member of Deuxfleurs\\
|
||||
Garage contributor, Freelance
|
||||
\end{column}
|
||||
\begin{column}{.4\textwidth}
|
||||
Eligible: email support, architecture design, specific feature development, etc.
|
||||
\end{column}
|
||||
\end{columns}
|
||||
|
||||
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Our initial goal}
|
||||
|
||||
\centering
|
||||
\Large
|
||||
|
||||
Being a self-sovereign community to be free of our degrowth choice
|
||||
|
||||
$\big\downarrow$
|
||||
|
||||
As web citizens, datacenters are big black boxes. \\
|
||||
We want to leave them to autonoumously manage our servers.
|
||||
|
||||
$\big\downarrow$
|
||||
|
||||
We want reliable services without relying on dedicated hardware or places.
|
||||
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Building a resilient system with cheap stuff}
|
||||
|
||||
\only<1,4-7>{
|
||||
\begin{itemize}
|
||||
\item \textcolor<5->{gray}{Commodity hardware (e.g. old desktop PCs)\\
|
||||
\vspace{.5em}
|
||||
\visible<4->{{\footnotesize (can die at any time)}}}
|
||||
\vspace{1.5em}
|
||||
\item<5-> \textcolor<7->{gray}{Regular Internet (e.g. FTTB, FTTH) and power grid connections\\
|
||||
\vspace{.5em}
|
||||
\visible<6->{{\footnotesize (can be unavailable randomly)}}}
|
||||
\vspace{1.5em}
|
||||
\item<7-> \textbf{Geographical redundancy} (multi-site replication)
|
||||
\end{itemize}
|
||||
}
|
||||
\only<2>{
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/neptune.jpg}
|
||||
\end{center}
|
||||
}
|
||||
\only<3>{
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/atuin.jpg}
|
||||
\end{center}
|
||||
}
|
||||
\only<8>{
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/inframap_jdll2023.pdf}
|
||||
\end{center}
|
||||
}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Object storage: a crucial component}
|
||||
\begin{center}
|
||||
\includegraphics[height=6em]{../assets/logos/Amazon-S3.jpg}
|
||||
\hspace{3em}
|
||||
\visible<2->{\includegraphics[height=5em]{../assets/logos/minio.png}}
|
||||
\hspace{3em}
|
||||
\visible<3>{\includegraphics[height=6em]{../../logo/garage_hires_crop.png}}
|
||||
\end{center}
|
||||
\vspace{1em}
|
||||
S3: a de-facto standard, many compatible applications
|
||||
|
||||
\vspace{1em}
|
||||
\visible<2->{MinIO is self-hostable but not suited for geo-distributed deployments}
|
||||
|
||||
\vspace{1em}
|
||||
\visible<3->{\textbf{Garage is a self-hosted drop-in replacement for the Amazon S3 object store}}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{CRDTs / weak consistency instead of consensus}
|
||||
|
||||
\underline{Internally, Garage uses only CRDTs} (conflict-free replicated data types)
|
||||
|
||||
\vspace{2em}
|
||||
Why not Raft, Paxos, ...? Issues of consensus algorithms:
|
||||
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item<2-> \textbf{Software complexity}
|
||||
\vspace{1em}
|
||||
\item<3-> \textbf{Performance issues:}
|
||||
\vspace{.5em}
|
||||
\begin{itemize}
|
||||
\item<4-> The leader is a \textbf{bottleneck} for all requests\\
|
||||
\vspace{.5em}
|
||||
\item<5-> \textbf{Sensitive to higher latency} between nodes
|
||||
\vspace{.5em}
|
||||
\item<6-> \textbf{Takes time to reconverge} when disrupted (e.g. node going down)
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{The data model of object storage}
|
||||
Object storage is basically a \textbf{key-value store}:
|
||||
\vspace{.5em}
|
||||
|
||||
{\scriptsize
|
||||
\begin{center}
|
||||
\begin{tabular}{|l|p{7cm}|}
|
||||
\hline
|
||||
\textbf{Key: file path + name} & \textbf{Value: file data + metadata} \\
|
||||
\hline
|
||||
\hline
|
||||
\texttt{index.html} &
|
||||
\texttt{Content-Type: text/html; charset=utf-8} \newline
|
||||
\texttt{Content-Length: 24929} \newline
|
||||
\texttt{<binary blob>} \\
|
||||
\hline
|
||||
\texttt{img/logo.svg} &
|
||||
\texttt{Content-Type: text/svg+xml} \newline
|
||||
\texttt{Content-Length: 13429} \newline
|
||||
\texttt{<binary blob>} \\
|
||||
\hline
|
||||
\texttt{download/index.html} &
|
||||
\texttt{Content-Type: text/html; charset=utf-8} \newline
|
||||
\texttt{Content-Length: 26563} \newline
|
||||
\texttt{<binary blob>} \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
}
|
||||
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item<2> Maps well to CRDT data types
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Performance gains in practice}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/perf/endpoint_latency_0.7_0.8_minio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
% ======================================== OPERATING
|
||||
% ======================================== OPERATING
|
||||
% ======================================== OPERATING
|
||||
|
||||
|
||||
\section{Production clusters}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Deployment kinds}
|
||||
|
||||
\includegraphics[width=.9\linewidth]{../assets/cluster_kind.png}
|
||||
\vspace{1em}
|
||||
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{How big they are?}
|
||||
|
||||
\includegraphics[width=.9\linewidth]{../assets/cluster_size.png}
|
||||
\vspace{1em}
|
||||
|
||||
\textit{"Petabyte storage setup for a video site. Nginx as CDN in-front using garage-s3-website feature. Each storage node has ~64TB storage with raid10, no replication within garage. 25gbit nic. haproxy to loadbalance across 5 nodes. mostly reads with very few writes."}
|
||||
|
||||
\vspace{1em}
|
||||
\textit{"We currently manage 7 Garage nodes, 28TB total storage, 6M blocks for 3M objects and 4TB of object data. We have been running Garage in production for 2.5 years."}
|
||||
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Operating Garage}
|
||||
\begin{center}
|
||||
\only<1-2>{
|
||||
\includegraphics[width=.9\linewidth]{../assets/screenshots/garage_status_0.10.png}
|
||||
\\\vspace{1em}
|
||||
\visible<2>{\includegraphics[width=.9\linewidth]{../assets/screenshots/garage_status_unhealthy_0.10.png}}
|
||||
}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Garage's architecture}
|
||||
\begin{center}
|
||||
\only<1>{\includegraphics[width=.45\linewidth]{../assets/garage.drawio.pdf}}%
|
||||
\only<2>{\includegraphics[width=.6\linewidth]{../assets/garage_sync.drawio.pdf}}%
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Digging deeper}
|
||||
\begin{center}
|
||||
\only<1>{\includegraphics[width=.9\linewidth]{../assets/screenshots/garage_stats_0.10.png}}
|
||||
\only<2>{\includegraphics[width=.5\linewidth]{../assets/screenshots/garage_worker_list_0.10.png}}
|
||||
\only<3>{\includegraphics[width=.6\linewidth]{../assets/screenshots/garage_worker_param_0.10.png}}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Potential limitations and bottlenecks}
|
||||
\begin{itemize}
|
||||
\item Global:
|
||||
\begin{itemize}
|
||||
\item Max. $\sim$100 nodes per cluster (excluding gateways)
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
\item Metadata:
|
||||
\begin{itemize}
|
||||
\item One big bucket = bottleneck, object list on 3 nodes only
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
\item Block manager:
|
||||
\begin{itemize}
|
||||
\item Lots of small files on disk
|
||||
\item Processing the resync queue can be slow
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Deployment advice for very large clusters}
|
||||
\begin{itemize}
|
||||
\item Metadata storage:
|
||||
\begin{itemize}
|
||||
\item ZFS mirror (x2) on fast NVMe
|
||||
\item Use LMDB storage engine
|
||||
\end{itemize}
|
||||
\vspace{.5em}
|
||||
\item Data block storage:
|
||||
\begin{itemize}
|
||||
\item Use Garage's native multi-HDD support
|
||||
\item XFS on individual drives
|
||||
\item Increase block size (1MB $\to$ 10MB, requires more RAM and good networking)
|
||||
\item Tune \texttt{resync-tranquility} and \texttt{resync-worker-count} dynamically
|
||||
\end{itemize}
|
||||
\vspace{.5em}
|
||||
\item Other :
|
||||
\begin{itemize}
|
||||
\item Split data over several buckets
|
||||
\item Use less than 100 storage nodes
|
||||
\item Use gateway nodes
|
||||
\end{itemize}
|
||||
\vspace{.5em}
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Focus on Deuxfleurs}
|
||||
|
||||
Host institutional websites, partnership with a web agency.
|
||||
Matrix media backend.
|
||||
|
||||
Plan to use it as an email backend for an internally developed email server.
|
||||
|
||||
\end{frame}
|
||||
|
||||
|
||||
% ======================================== TIMELINE
|
||||
% ======================================== TIMELINE
|
||||
% ======================================== TIMELINE
|
||||
|
||||
\section{Recent developments}
|
||||
|
||||
% ====================== v0.7.0 ===============================
|
||||
|
||||
\begin{frame}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/tl.drawio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{April 2022 - Garage v0.7.0}
|
||||
Focus on \underline{observability and ecosystem integration}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item \textbf{Monitoring:} metrics and traces, using OpenTelemetry
|
||||
\vspace{1em}
|
||||
\item Replication modes with 1 or 2 copies / weaker consistency
|
||||
\vspace{1em}
|
||||
\item Kubernetes integration for node discovery
|
||||
\vspace{1em}
|
||||
\item Admin API (v0.7.2)
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Metrics (Prometheus + Grafana)}
|
||||
\begin{center}
|
||||
\includegraphics[width=.9\linewidth]{../assets/screenshots/grafana_dashboard.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Traces (Jaeger)}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/screenshots/jaeger_listobjects.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
% ====================== v0.8.0 ===============================
|
||||
|
||||
\begin{frame}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/tl.drawio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{November 2022 - Garage v0.8.0}
|
||||
Focus on \underline{performance}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item \textbf{Alternative metadata DB engines} (LMDB, Sqlite)
|
||||
\vspace{1em}
|
||||
\item \textbf{Performance improvements:} block streaming, various optimizations...
|
||||
\vspace{1em}
|
||||
\item Bucket quotas (max size, max \#objects)
|
||||
\vspace{1em}
|
||||
\item Quality of life improvements, observability, etc.
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{About metadata DB engines}
|
||||
\textbf{Issues with Sled:}
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item Huge files on disk
|
||||
\vspace{.5em}
|
||||
\item Unpredictable performance, especially on HDD
|
||||
\vspace{.5em}
|
||||
\item API limitations
|
||||
\vspace{.5em}
|
||||
\item Not actively maintained
|
||||
\end{itemize}
|
||||
|
||||
\vspace{2em}
|
||||
\textbf{LMDB:} very stable, good performance, file size is reasonable\\
|
||||
\textbf{Sqlite} also available as a second choice
|
||||
|
||||
\vspace{1em}
|
||||
Sled will be removed in Garage v1.0
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{DB engine performance comparison}
|
||||
\begin{center}
|
||||
\includegraphics[width=.6\linewidth]{../assets/perf/db_engine.png}
|
||||
\end{center}
|
||||
NB: Sqlite was slow due to synchronous mode, now configurable
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Block streaming}
|
||||
\begin{center}
|
||||
\only<1>{\includegraphics[width=.8\linewidth]{../assets/schema-streaming-1.png}}
|
||||
\only<2>{\includegraphics[width=.8\linewidth]{../assets/schema-streaming-2.png}}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{TTFB benchmark}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/perf/ttfb.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Throughput benchmark}
|
||||
\begin{center}
|
||||
\includegraphics[width=.7\linewidth]{../assets/perf/io-0.7-0.8-minio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
% ====================== v0.9.0 ===============================
|
||||
|
||||
\begin{frame}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/tl.drawio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{October 2023 - Garage v0.9.0}
|
||||
Focus on \underline{streamlining \& usability}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item Support multiple HDDs per node
|
||||
\vspace{1em}
|
||||
\item S3 compatibility:
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item support basic lifecycle configurations
|
||||
\vspace{.5em}
|
||||
\item allow for multipart upload part retries
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
\item LMDB by default, deprecation of Sled
|
||||
\vspace{1em}
|
||||
\item New layout computation algorithm
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Layout computation}
|
||||
\begin{overprint}
|
||||
\onslide<1>
|
||||
\begin{center}
|
||||
\includegraphics[width=\linewidth, trim=0 0 0 -4cm]{../assets/screenshots/garage_status_0.9_prod_zonehl.png}
|
||||
\end{center}
|
||||
\onslide<2>
|
||||
\begin{center}
|
||||
\includegraphics[width=.7\linewidth]{../assets/map.png}
|
||||
\end{center}
|
||||
\end{overprint}
|
||||
\vspace{1em}
|
||||
Garage stores replicas on different zones when possible
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{What a "layout" is}
|
||||
\textbf{A layout is a precomputed index table:}
|
||||
\vspace{1em}
|
||||
|
||||
{\footnotesize
|
||||
\begin{center}
|
||||
\begin{tabular}{|l|l|l|l|}
|
||||
\hline
|
||||
\textbf{Partition} & \textbf{Node 1} & \textbf{Node 2} & \textbf{Node 3} \\
|
||||
\hline
|
||||
\hline
|
||||
Partition 0 & df-ymk (bespin) & Abricot (scorpio) & Courgette (neptune) \\
|
||||
\hline
|
||||
Partition 1 & Ananas (scorpio) & Courgette (neptune) & df-ykl (bespin) \\
|
||||
\hline
|
||||
Partition 2 & df-ymf (bespin) & Celeri (neptune) & Abricot (scorpio) \\
|
||||
\hline
|
||||
\hspace{1em}$\vdots$ & \hspace{1em}$\vdots$ & \hspace{1em}$\vdots$ & \hspace{1em}$\vdots$ \\
|
||||
\hline
|
||||
Partition 255 & Concombre (neptune) & df-ykl (bespin) & Abricot (scorpio) \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
}
|
||||
|
||||
\vspace{2em}
|
||||
\visible<2->{
|
||||
The index table is built centrally using an optimal algorithm,\\
|
||||
then propagated to all nodes
|
||||
}
|
||||
|
||||
\vspace{1em}
|
||||
\visible<3->{
|
||||
\footnotesize
|
||||
Oulamara, M., \& Auvolat, A. (2023). \emph{An algorithm for geo-distributed and redundant storage in Garage}.\\ arXiv preprint arXiv:2302.13798.
|
||||
}
|
||||
\end{frame}
|
||||
|
||||
|
||||
|
||||
% ====================== v1.0.0 ===============================
|
||||
|
||||
\begin{frame}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/tl.drawio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{April 2024 - Garage v1.0.0}
|
||||
Focus on \underline{consistency, security \& stability}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item Fix consistency issues when reshuffling data (Jepsen testing)
|
||||
\vspace{1em}
|
||||
\item \textbf{Security audit} by Radically Open Security
|
||||
\vspace{1em}
|
||||
\item Misc. S3 features (SSE-C, checksums, ...) and compatibility fixes
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
% ====================== v2.0.0 ===============================
|
||||
|
||||
\begin{frame}
|
||||
\begin{center}
|
||||
\includegraphics[width=.8\linewidth]{../assets/tl.drawio.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Garage v2.0.0}
|
||||
Focus on \underline{}
|
||||
\vspace{2em}
|
||||
\begin{itemize}
|
||||
\item TODO
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Currently funding...}
|
||||
|
||||
\textit{...}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{We run community surveys}
|
||||
\begin{center}
|
||||
\includegraphics[width=.6\linewidth]{../assets/survey_requested_features.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
% ======================================== END
|
||||
% ======================================== END
|
||||
% ======================================== END
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Where to find us}
|
||||
\begin{center}
|
||||
\includegraphics[width=.25\linewidth]{../../logo/garage_hires.png}\\
|
||||
\vspace{-1em}
|
||||
\url{https://garagehq.deuxfleurs.fr/}\\
|
||||
\url{mailto:garagehq@deuxfleurs.fr}\\
|
||||
\texttt{\#garage:deuxfleurs.fr} on Matrix
|
||||
|
||||
\vspace{1.5em}
|
||||
\includegraphics[width=.06\linewidth]{../assets/logos/rust_logo.png}
|
||||
\includegraphics[width=.13\linewidth]{../assets/logos/AGPLv3_Logo.png}
|
||||
\end{center}
|
||||
\end{frame}
|
||||
|
||||
\end{document}
|
||||
|
||||
%% vim: set ts=4 sw=4 tw=0 noet spelllang=en :
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 183 KiB |
@@ -1,38 +1,28 @@
|
||||
{
|
||||
"nodes": {
|
||||
"cargo2nix": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"flake-compat"
|
||||
],
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-overlay": "rust-overlay"
|
||||
},
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1666087781,
|
||||
"narHash": "sha256-trKVdjMZ8mNkGfLcY5LsJJGtdV3xJDZnMVrkFjErlcs=",
|
||||
"owner": "Alexis211",
|
||||
"repo": "cargo2nix",
|
||||
"rev": "a7a61179b66054904ef6a195d8da736eaaa06c36",
|
||||
"lastModified": 1737689766,
|
||||
"narHash": "sha256-ivVXYaYlShxYoKfSo5+y5930qMKKJ8CLcAoIBPQfJ6s=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "6fe74265bbb6d016d663b1091f015e2976c4a527",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "Alexis211",
|
||||
"repo": "cargo2nix",
|
||||
"rev": "a7a61179b66054904ef6a195d8da736eaaa06c36",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "6fe74265bbb6d016d663b1091f015e2976c4a527",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"locked": {
|
||||
"lastModified": 1688025799,
|
||||
"narHash": "sha256-ktpB4dRtnksm9F5WawoIkEneh1nrEvuxb5lJFt1iOyw=",
|
||||
"lastModified": 1761640442,
|
||||
"narHash": "sha256-AtrEP6Jmdvrqiv4x2xa5mrtaIp3OEe8uBYCDZDS+hu8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "flake-compat",
|
||||
"rev": "8bf105319d44f6b9f0d764efa4fdef9f1cc9ba1c",
|
||||
"rev": "4a56054d8ffc173222d09dad23adf4ba946c8884",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -46,29 +36,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -79,63 +51,47 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1682109806,
|
||||
"narHash": "sha256-d9g7RKNShMLboTWwukM+RObDWWpHKaqTYXB48clBWXI=",
|
||||
"lastModified": 1763977559,
|
||||
"narHash": "sha256-g4MKqsIRy5yJwEsI+fYODqLUnAqIY4kZai0nldAP6EM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2362848adf8def2866fabbffc50462e929d7fffb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1707091808,
|
||||
"narHash": "sha256-LahKBAfGbY836gtpVNnWwBTIzN7yf/uYM/S0g393r0Y=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9f2ee8c91ac42da3ae6c6a1d21555f283458247e",
|
||||
"rev": "cfe2c7d5b5d3032862254e68c37a6576b633d632",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9f2ee8c91ac42da3ae6c6a1d21555f283458247e",
|
||||
"rev": "cfe2c7d5b5d3032862254e68c37a6576b633d632",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"cargo2nix": "cargo2nix",
|
||||
"crane": "crane",
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": [
|
||||
"cargo2nix",
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1707271822,
|
||||
"narHash": "sha256-/DZsoPH5GBzOpVEGz5PgJ7vh8Q6TcrJq5u8FcBjqAfI=",
|
||||
"lastModified": 1763952169,
|
||||
"narHash": "sha256-+PeDBD8P+NKauH+w7eO/QWCIp8Cx4mCfWnh9sJmy9CM=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "7a94fe7690d2bdfe1aab475382a505e14dc114a6",
|
||||
"rev": "ab726555a9a72e6dc80649809147823a813fa95b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "ab726555a9a72e6dc80649809147823a813fa95b",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
@@ -153,21 +109,6 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -2,88 +2,99 @@
|
||||
description =
|
||||
"Garage, an S3-compatible distributed object store for self-hosted deployments";
|
||||
|
||||
# Nixpkgs 23.11 as of 2024-02-07, has rustc v1.73
|
||||
# Nixpkgs 25.05 as of 2025-11-24
|
||||
inputs.nixpkgs.url =
|
||||
"github:NixOS/nixpkgs/9f2ee8c91ac42da3ae6c6a1d21555f283458247e";
|
||||
"github:NixOS/nixpkgs/cfe2c7d5b5d3032862254e68c37a6576b633d632";
|
||||
|
||||
# Rust overlay as of 2025-11-24
|
||||
inputs.rust-overlay.url =
|
||||
"github:oxalica/rust-overlay/ab726555a9a72e6dc80649809147823a813fa95b";
|
||||
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
# Crane as of 2025-01-24
|
||||
inputs.crane.url = "github:ipetkov/crane/6fe74265bbb6d016d663b1091f015e2976c4a527";
|
||||
|
||||
inputs.flake-compat.url = "github:nix-community/flake-compat";
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
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";
|
||||
|
||||
# As of 2023-04-25:
|
||||
# - my two patches were merged into unstable (one for clippy and one to "fix" feature detection)
|
||||
# - rustc v1.66
|
||||
# url = "github:cargo2nix/cargo2nix/8fb57a670f7993bfc24099c33eb9c5abb51f29a2";
|
||||
|
||||
# Rust overlay as of 2024-02-07
|
||||
inputs.rust-overlay.url =
|
||||
"github:oxalica/rust-overlay/7a94fe7690d2bdfe1aab475382a505e14dc114a6";
|
||||
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.flake-compat.follows = "flake-compat";
|
||||
};
|
||||
|
||||
inputs.flake-utils.follows = "cargo2nix/flake-utils";
|
||||
|
||||
outputs = { self, nixpkgs, cargo2nix, flake-utils, ... }:
|
||||
outputs = { self, nixpkgs, flake-utils, crane, rust-overlay, ... }:
|
||||
let
|
||||
git_version = self.lastModifiedDate;
|
||||
compile = import ./nix/compile.nix;
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
packageFor = target: release: (compile {
|
||||
inherit system target nixpkgs crane rust-overlay release;
|
||||
}).garage;
|
||||
testWith = extraTestEnv: (compile {
|
||||
inherit system nixpkgs crane rust-overlay extraTestEnv;
|
||||
release = false;
|
||||
}).garage-test;
|
||||
lints = (compile {
|
||||
inherit system nixpkgs crane rust-overlay;
|
||||
release = false;
|
||||
});
|
||||
in
|
||||
{
|
||||
packages =
|
||||
let
|
||||
packageFor = target: (compile {
|
||||
inherit system git_version target;
|
||||
pkgsSrc = nixpkgs;
|
||||
cargo2nixOverlay = cargo2nix.overlays.default;
|
||||
release = true;
|
||||
}).workspace.garage { compileMode = "build"; };
|
||||
in
|
||||
{
|
||||
# default = native release build
|
||||
default = packageFor null;
|
||||
# other = cross-compiled, statically-linked builds
|
||||
amd64 = packageFor "x86_64-unknown-linux-musl";
|
||||
i386 = packageFor "i686-unknown-linux-musl";
|
||||
arm64 = packageFor "aarch64-unknown-linux-musl";
|
||||
arm = packageFor "armv6l-unknown-linux-musl";
|
||||
packages = {
|
||||
# default = native release build
|
||||
default = packageFor null true;
|
||||
|
||||
# <arch> = cross-compiled, statically-linked release builds
|
||||
amd64 = packageFor "x86_64-unknown-linux-musl" true;
|
||||
i386 = packageFor "i686-unknown-linux-musl" true;
|
||||
arm64 = packageFor "aarch64-unknown-linux-musl" true;
|
||||
arm = packageFor "armv6l-unknown-linux-musl" true;
|
||||
|
||||
# dev = native dev build
|
||||
dev = packageFor null false;
|
||||
|
||||
# test = cargo test
|
||||
tests = testWith {};
|
||||
tests-lmdb = testWith {
|
||||
GARAGE_TEST_INTEGRATION_DB_ENGINE = "lmdb";
|
||||
};
|
||||
tests-sqlite = testWith {
|
||||
GARAGE_TEST_INTEGRATION_DB_ENGINE = "sqlite";
|
||||
};
|
||||
tests-fjall = testWith {
|
||||
GARAGE_TEST_INTEGRATION_DB_ENGINE = "fjall";
|
||||
};
|
||||
|
||||
# lints (fmt, clippy)
|
||||
fmt = lints.garage-cargo-fmt;
|
||||
clippy = lints.garage-cargo-clippy;
|
||||
};
|
||||
|
||||
# ---- developpment shell, for making native builds only ----
|
||||
devShells =
|
||||
let
|
||||
shellWithPackages = (packages: (compile {
|
||||
inherit system git_version;
|
||||
pkgsSrc = nixpkgs;
|
||||
cargo2nixOverlay = cargo2nix.overlays.default;
|
||||
}).workspaceShell { inherit packages; });
|
||||
targets = compile {
|
||||
inherit system nixpkgs crane rust-overlay;
|
||||
};
|
||||
in
|
||||
{
|
||||
default = shellWithPackages
|
||||
(with pkgs; [
|
||||
rustfmt
|
||||
clang
|
||||
mold
|
||||
]);
|
||||
default = targets.devShell;
|
||||
|
||||
# import the full shell using `nix develop .#full`
|
||||
full = shellWithPackages (with pkgs; [
|
||||
rustfmt
|
||||
clang
|
||||
mold
|
||||
# ---- extra packages for dev tasks ----
|
||||
cargo-audit
|
||||
cargo-outdated
|
||||
cargo-machete
|
||||
nixpkgs-fmt
|
||||
]);
|
||||
full = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
targets.toolchain
|
||||
protobuf
|
||||
clang
|
||||
mold
|
||||
# ---- extra packages for dev tasks ----
|
||||
rust-analyzer
|
||||
cargo-audit
|
||||
cargo-outdated
|
||||
cargo-machete
|
||||
nixpkgs-fmt
|
||||
openssl
|
||||
socat
|
||||
killall
|
||||
];
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
with import ./common.nix;
|
||||
let
|
||||
pkgs = import pkgsSrc { };
|
||||
pkgs = import nixpkgs { };
|
||||
lib = pkgs.lib;
|
||||
|
||||
/* Converts a key list and a value list to a set
|
||||
@@ -167,7 +167,7 @@ let
|
||||
</ul></p>
|
||||
<p> Sources:
|
||||
<ul>
|
||||
<li><a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/src/${r.type}/${x.version}">gitea</a></li>
|
||||
<li><a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/src/${r.type}/${x.version}">Forgejo</a></li>
|
||||
<li><a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/archive/${x.version}.zip">.zip</a></li>
|
||||
<li><a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/archive/${x.version}.tar.gz">.tar.gz</a></li>
|
||||
</ul></p>
|
||||
|
||||
@@ -10,9 +10,9 @@ let
|
||||
|
||||
flake = (import flake-compat { system = builtins.currentSystem; src = ../.; });
|
||||
in
|
||||
rec {
|
||||
pkgsSrc = flake.defaultNix.inputs.nixpkgs;
|
||||
cargo2nix = flake.defaultNix.inputs.cargo2nix;
|
||||
cargo2nixOverlay = cargo2nix.overlays.default;
|
||||
devShells = builtins.getAttr builtins.currentSystem flake.defaultNix.devShells;
|
||||
|
||||
{
|
||||
flake = flake.defaultNix;
|
||||
nixpkgs = flake.defaultNix.inputs.nixpkgs;
|
||||
devShells = flake.defaultNix.devShells.${builtins.currentSystem};
|
||||
}
|
||||
|
||||
@@ -1,164 +1,64 @@
|
||||
{ system, target ? null, pkgsSrc, cargo2nixOverlay, compiler ? "rustc"
|
||||
, release ? false, git_version ? null, features ? null, }:
|
||||
{
|
||||
/* build inputs */
|
||||
nixpkgs,
|
||||
crane,
|
||||
rust-overlay,
|
||||
|
||||
/* parameters */
|
||||
system,
|
||||
git_version ? null,
|
||||
target ? null,
|
||||
release ? false,
|
||||
features ? null,
|
||||
extraTestEnv ? {}
|
||||
}:
|
||||
|
||||
let
|
||||
log = v: builtins.trace v v;
|
||||
|
||||
# NixOS and Rust/Cargo triples do not match for ARM, fix it here.
|
||||
rustTarget = if target == "armv6l-unknown-linux-musleabihf" then
|
||||
"arm-unknown-linux-musleabihf"
|
||||
else
|
||||
target;
|
||||
|
||||
rustTargetEnvMap = {
|
||||
"x86_64-unknown-linux-musl" = "X86_64_UNKNOWN_LINUX_MUSL";
|
||||
"aarch64-unknown-linux-musl" = "AARCH64_UNKNOWN_LINUX_MUSL";
|
||||
"i686-unknown-linux-musl" = "I686_UNKNOWN_LINUX_MUSL";
|
||||
"arm-unknown-linux-musleabihf" = "ARM_UNKNOWN_LINUX_MUSLEABIHF";
|
||||
};
|
||||
|
||||
pkgsNative = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
|
||||
pkgs = if target != null then
|
||||
import pkgsSrc {
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
crossSystem = {
|
||||
config = target;
|
||||
isStatic = true;
|
||||
};
|
||||
overlays = [ cargo2nixOverlay ];
|
||||
overlays = [ (import rust-overlay) ];
|
||||
}
|
||||
else
|
||||
import pkgsSrc {
|
||||
inherit system;
|
||||
overlays = [ cargo2nixOverlay ];
|
||||
};
|
||||
pkgsNative;
|
||||
|
||||
toolchainOptions = {
|
||||
rustVersion = "1.73.0";
|
||||
extraRustComponents = [ "clippy" ];
|
||||
};
|
||||
inherit (pkgs) lib stdenv;
|
||||
|
||||
buildEnv = (drv:
|
||||
{
|
||||
rustc = drv.setBuildEnv;
|
||||
clippy = ''
|
||||
${drv.setBuildEnv or ""}
|
||||
echo
|
||||
echo --- BUILDING WITH CLIPPY ---
|
||||
echo
|
||||
|
||||
export NIX_RUST_BUILD_FLAGS="''${NIX_RUST_BUILD_FLAGS} --deny warnings"
|
||||
export RUSTC="''${CLIPPY_DRIVER}"
|
||||
'';
|
||||
}.${compiler});
|
||||
|
||||
/* Cargo2nix provides many overrides by default, you can take inspiration from them:
|
||||
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/overrides.nix
|
||||
|
||||
You can have a complete list of the available options by looking at the overriden object, mkcrate:
|
||||
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/mkcrate.nix
|
||||
*/
|
||||
packageOverrides = pkgs:
|
||||
pkgs.rustBuilder.overrides.all ++ [
|
||||
/* [1] We add some logic to compile our crates with clippy, it provides us many additional lints
|
||||
|
||||
[2] We need to alter Nix hardening to make static binaries: PIE,
|
||||
Position Independent Executables seems to be supported only on amd64. Having
|
||||
this flag set either 1. make our executables crash or 2. compile as dynamic on some platforms.
|
||||
Here, we deactivate it. Later (find `codegenOpts`), we reactivate it for supported targets
|
||||
(only amd64 curently) through the `-static-pie` flag.
|
||||
PIE is a feature used by ASLR, which helps mitigate security issues.
|
||||
Learn more about Nix Hardening at: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh
|
||||
|
||||
[3] We want to inject the git version while keeping the build deterministic.
|
||||
As we do not want to consider the .git folder as part of the input source,
|
||||
we ask the user (the CI often) to pass the value to Nix.
|
||||
|
||||
[4] We don't want libsodium-sys and zstd-sys to try to use pkgconfig to build against a system library.
|
||||
However the features to do so get activated for some reason (due to a bug in cargo2nix?),
|
||||
so disable them manually here.
|
||||
*/
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage";
|
||||
overrideAttrs = drv:
|
||||
(if git_version != null then {
|
||||
# [3]
|
||||
preConfigure = ''
|
||||
${drv.preConfigure or ""}
|
||||
export GIT_VERSION="${git_version}"
|
||||
'';
|
||||
} else
|
||||
{ }) // {
|
||||
# [1]
|
||||
setBuildEnv = (buildEnv drv);
|
||||
# [2]
|
||||
hardeningDisable = [ "pie" ];
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_rpc";
|
||||
overrideAttrs = drv: { # [1]
|
||||
setBuildEnv = (buildEnv drv);
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_db";
|
||||
overrideAttrs = drv: { # [1]
|
||||
setBuildEnv = (buildEnv drv);
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_util";
|
||||
overrideAttrs = drv: { # [1]
|
||||
setBuildEnv = (buildEnv drv);
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_table";
|
||||
overrideAttrs = drv: { # [1]
|
||||
setBuildEnv = (buildEnv drv);
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_block";
|
||||
overrideAttrs = drv: { # [1]
|
||||
setBuildEnv = (buildEnv drv);
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_model";
|
||||
overrideAttrs = drv: { # [1]
|
||||
setBuildEnv = (buildEnv drv);
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_api";
|
||||
overrideAttrs = drv: { # [1]
|
||||
setBuildEnv = (buildEnv drv);
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "garage_web";
|
||||
overrideAttrs = drv: { # [1]
|
||||
setBuildEnv = (buildEnv drv);
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "k2v-client";
|
||||
overrideAttrs = drv: { # [1]
|
||||
setBuildEnv = (buildEnv drv);
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "libsodium-sys";
|
||||
overrideArgs = old: {
|
||||
features = [ ]; # [4]
|
||||
};
|
||||
})
|
||||
|
||||
(pkgs.rustBuilder.rustLib.makeOverride {
|
||||
name = "zstd-sys";
|
||||
overrideArgs = old: {
|
||||
features = [ ]; # [4]
|
||||
};
|
||||
})
|
||||
toolchainFn = (p: p.rust-bin.stable."1.91.0".default.override {
|
||||
targets = lib.optionals (target != null) [ rustTarget ];
|
||||
extensions = [
|
||||
"rust-src"
|
||||
"rustfmt"
|
||||
];
|
||||
});
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain toolchainFn;
|
||||
|
||||
src = craneLib.cleanCargoSource ../.;
|
||||
|
||||
/* We ship some parts of the code disabled by default by putting them behind a flag.
|
||||
It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.).
|
||||
@@ -168,16 +68,16 @@ let
|
||||
rootFeatures = if features != null then
|
||||
features
|
||||
else
|
||||
([ "garage/bundled-libs" "garage/lmdb" "garage/sqlite" "garage/k2v" ] ++ (if release then [
|
||||
"garage/consul-discovery"
|
||||
"garage/kubernetes-discovery"
|
||||
"garage/metrics"
|
||||
"garage/telemetry-otlp"
|
||||
"garage/syslog"
|
||||
] else
|
||||
[ ]));
|
||||
([ "bundled-libs" "lmdb" "sqlite" "fjall" "k2v" ] ++ (lib.optionals release [
|
||||
"consul-discovery"
|
||||
"kubernetes-discovery"
|
||||
"metrics"
|
||||
"telemetry-otlp"
|
||||
"syslog"
|
||||
"journald"
|
||||
]));
|
||||
|
||||
packageFun = import ../Cargo.nix;
|
||||
featuresStr = lib.concatStringsSep "," rootFeatures;
|
||||
|
||||
/* We compile fully static binaries with musl to simplify deployment on most systems.
|
||||
When possible, we reactivate PIE hardening (see above).
|
||||
@@ -188,12 +88,9 @@ let
|
||||
For more information on static builds, please refer to Rust's RFC 1721.
|
||||
https://rust-lang.github.io/rfcs/1721-crt-static.html#specifying-dynamicstatic-c-runtime-linkage
|
||||
*/
|
||||
|
||||
codegenOpts = {
|
||||
"armv6l-unknown-linux-musleabihf" = [
|
||||
"target-feature=+crt-static"
|
||||
"link-arg=-static"
|
||||
]; # compile as dynamic with static-pie
|
||||
codegenOptsMap = {
|
||||
"x86_64-unknown-linux-musl" =
|
||||
[ "target-feature=+crt-static" "link-arg=-static-pie" ];
|
||||
"aarch64-unknown-linux-musl" = [
|
||||
"target-feature=+crt-static"
|
||||
"link-arg=-static"
|
||||
@@ -202,17 +99,106 @@ let
|
||||
"target-feature=+crt-static"
|
||||
"link-arg=-static"
|
||||
]; # segfault with static-pie
|
||||
"x86_64-unknown-linux-musl" =
|
||||
[ "target-feature=+crt-static" "link-arg=-static-pie" ];
|
||||
"armv6l-unknown-linux-musleabihf" = [
|
||||
"target-feature=+crt-static"
|
||||
"link-arg=-static"
|
||||
]; # compile as dynamic with static-pie
|
||||
};
|
||||
|
||||
# NixOS and Rust/Cargo triples do not match for ARM, fix it here.
|
||||
rustTarget = if target == "armv6l-unknown-linux-musleabihf" then
|
||||
"arm-unknown-linux-musleabihf"
|
||||
else
|
||||
target;
|
||||
codegenOpts = if target != null then codegenOptsMap.${target} else [
|
||||
"link-arg=-fuse-ld=mold"
|
||||
];
|
||||
|
||||
in pkgs.rustBuilder.makePackageSet ({
|
||||
inherit release packageFun packageOverrides codegenOpts rootFeatures;
|
||||
target = rustTarget;
|
||||
} // toolchainOptions)
|
||||
commonArgs =
|
||||
{
|
||||
inherit src;
|
||||
pname = "garage";
|
||||
version = "dev";
|
||||
|
||||
strictDeps = true;
|
||||
cargoExtraArgs = "--locked --features ${featuresStr}";
|
||||
cargoTestExtraArgs = "--workspace";
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgsNative.protobuf
|
||||
pkgs.stdenv.cc
|
||||
] ++ lib.optionals (target == null) [
|
||||
pkgs.clang
|
||||
pkgs.mold
|
||||
];
|
||||
|
||||
CARGO_PROFILE = if release then "release" else "dev";
|
||||
CARGO_BUILD_RUSTFLAGS =
|
||||
lib.concatStringsSep
|
||||
" "
|
||||
(builtins.map (flag: "-C ${flag}") codegenOpts);
|
||||
}
|
||||
//
|
||||
(if rustTarget != null then {
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
|
||||
"CARGO_TARGET_${rustTargetEnvMap.${rustTarget}}_LINKER" = "${stdenv.cc.targetPrefix}cc";
|
||||
|
||||
HOST_CC = "${stdenv.cc.nativePrefix}cc";
|
||||
TARGET_CC = "${stdenv.cc.targetPrefix}cc";
|
||||
} else {
|
||||
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = "clang";
|
||||
});
|
||||
|
||||
in rec {
|
||||
toolchain = toolchainFn pkgs;
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = [
|
||||
toolchain
|
||||
] ++ (with pkgs; [
|
||||
protobuf
|
||||
clang
|
||||
mold
|
||||
]);
|
||||
};
|
||||
|
||||
# ---- building garage ----
|
||||
|
||||
garage-deps = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
garage = craneLib.buildPackage (commonArgs // {
|
||||
cargoArtifacts = garage-deps;
|
||||
|
||||
doCheck = false;
|
||||
} //
|
||||
(if git_version != null then {
|
||||
version = git_version;
|
||||
GIT_VERSION = git_version;
|
||||
} else {}));
|
||||
|
||||
# ---- testing garage ----
|
||||
|
||||
garage-test-bin = craneLib.cargoBuild (commonArgs // {
|
||||
cargoArtifacts = garage-deps;
|
||||
|
||||
pname = "garage-tests";
|
||||
|
||||
CARGO_PROFILE = "test";
|
||||
cargoExtraArgs = "${commonArgs.cargoExtraArgs} --tests --workspace";
|
||||
doCheck = false;
|
||||
});
|
||||
|
||||
garage-test = craneLib.cargoTest (commonArgs // {
|
||||
cargoArtifacts = garage-test-bin;
|
||||
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [
|
||||
pkgs.cacert
|
||||
];
|
||||
} // extraTestEnv);
|
||||
|
||||
# ---- source code linting ----
|
||||
|
||||
garage-cargo-fmt = craneLib.cargoFmt (commonArgs // {
|
||||
cargoExtraArgs = "";
|
||||
});
|
||||
|
||||
garage-cargo-clippy = craneLib.cargoClippy (commonArgs // {
|
||||
cargoArtifacts = garage-deps;
|
||||
cargoClippyExtraArgs = "--all-targets -- -D warnings";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,13 +17,19 @@ else
|
||||
fi
|
||||
|
||||
$GARAGE_BIN -c /tmp/config.1.toml bucket create eprouvette
|
||||
if [ "$GARAGE_08" = "1" ]; then
|
||||
if [ "$GARAGE_OLDVER" = "v08" ]; then
|
||||
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml key new --name opérateur)
|
||||
else
|
||||
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
|
||||
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
|
||||
elif [ "$GARAGE_OLDVER" = "v1" ]; then
|
||||
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml key create opérateur)
|
||||
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
|
||||
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
|
||||
else
|
||||
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml json-api CreateKey '{"name":"opérateur"}')
|
||||
ACCESS_KEY=`echo $KEY_INFO|jq -r .accessKeyId`
|
||||
SECRET_KEY=`echo $KEY_INFO|jq -r .secretAccessKey`
|
||||
fi
|
||||
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
|
||||
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
|
||||
$GARAGE_BIN -c /tmp/config.1.toml bucket allow eprouvette --read --write --owner --key $ACCESS_KEY
|
||||
echo "$ACCESS_KEY $SECRET_KEY" > /tmp/garage.s3
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:${NIX_RELEASE}:$PATH"
|
||||
FANCYCOLORS=("41m" "42m" "44m" "45m" "100m" "104m")
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
export RUST_LOG=garage=info,garage_api=debug
|
||||
export RUST_LOG=garage=info,garage_api_common=debug,garage_api_s3=debug
|
||||
MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m"
|
||||
|
||||
if [ -z "$GARAGE_BIN" ]; then
|
||||
@@ -30,6 +30,12 @@ for count in $(seq 1 3); do
|
||||
CONF_PATH="/tmp/config.$count.toml"
|
||||
LABEL="\e[${FANCYCOLORS[$count]}[$count]\e[49m"
|
||||
|
||||
if [ "$GARAGE_OLDVER" == "v08" ]; then
|
||||
REPLICATION_MODE="replication_mode = \"3\""
|
||||
else
|
||||
REPLICATION_MODE="replication_factor = 3"
|
||||
fi
|
||||
|
||||
cat > $CONF_PATH <<EOF
|
||||
block_size = 1048576 # objects are split in blocks of maximum this number of bytes
|
||||
metadata_dir = "/tmp/garage-meta-$count"
|
||||
@@ -38,7 +44,7 @@ data_dir = "/tmp/garage-data-$count"
|
||||
rpc_bind_addr = "0.0.0.0:$((3900+$count))" # the port other Garage nodes will use to talk to this node
|
||||
rpc_public_addr = "127.0.0.1:$((3900+$count))"
|
||||
bootstrap_peers = []
|
||||
replication_mode = "3"
|
||||
$REPLICATION_MODE
|
||||
rpc_secret = "$NETWORK_SECRET"
|
||||
|
||||
[s3_api]
|
||||
|
||||
@@ -29,7 +29,7 @@ until $GARAGE_BIN -c /tmp/config.1.toml status 2>&1|grep -q HEALTHY ; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ "$GARAGE_08" = "1" ]; then
|
||||
if [ "$GARAGE_OLDVER" = "v08" ]; then
|
||||
$GARAGE_BIN -c /tmp/config.1.toml status \
|
||||
| grep 'NO ROLE' \
|
||||
| grep -Po '^[0-9a-f]+' \
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Garage helm3 chart
|
||||
|
||||
Documentation is located [here](/doc/book/cookbook/kubernetes.md).
|
||||
Documentation is located [here](https://garagehq.deuxfleurs.fr/documentation/cookbook/kubernetes/).
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
apiVersion: v2
|
||||
name: garage
|
||||
description: S3-compatible object store for small self-hosted geo-distributed deployments
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
version: 0.9.2
|
||||
appVersion: "v2.2.0"
|
||||
home: https://garagehq.deuxfleurs.fr/
|
||||
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.5.0
|
||||
keywords:
|
||||
- geo-distributed
|
||||
- read-after-write-consistency
|
||||
- s3-compatible
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v1.0.0"
|
||||
sources:
|
||||
- https://git.deuxfleurs.fr/Deuxfleurs/garage.git
|
||||
|
||||
maintainers: []
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# garage
|
||||
|
||||
  
|
||||
|
||||
S3-compatible object store for small self-hosted geo-distributed deployments
|
||||
|
||||
**Homepage:** <https://garagehq.deuxfleurs.fr/>
|
||||
|
||||
## Source Code
|
||||
|
||||
* <https://git.deuxfleurs.fr/Deuxfleurs/garage.git>
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| affinity | object | `{}` | |
|
||||
| commonLabels | object | `{}` | Extra labels for all resources |
|
||||
| deployment.kind | string | `"StatefulSet"` | Switchable to DaemonSet |
|
||||
| deployment.podManagementPolicy | string | `"OrderedReady"` | If using statefulset, allow Parallel or OrderedReady (default) |
|
||||
| deployment.replicaCount | int | `3` | Number of StatefulSet replicas/garage nodes to start |
|
||||
| environment | object | `{}` | |
|
||||
| extraVolumeMounts | object | `{}` | |
|
||||
| extraVolumes | object | `{}` | |
|
||||
| fullnameOverride | string | `""` | |
|
||||
| garage.blockSize | string | `"1048576"` | Defaults is 1MB An increase can result in better performance in certain scenarios https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block_size |
|
||||
| garage.bootstrapPeers | list | `[]` | This is not required if you use the integrated kubernetes discovery |
|
||||
| garage.compressionLevel | string | `"1"` | zstd compression level of stored blocks https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression_level |
|
||||
| garage.dbEngine | string | `"lmdb"` | Can be changed for better performance on certain systems https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine |
|
||||
| garage.existingConfigMap | string | `""` | if not empty string, allow using an existing ConfigMap for the garage.toml, if set, ignores garage.toml |
|
||||
| garage.garageTomlString | string | `""` | String Template for the garage configuration if set, ignores above values. Values can be templated, see https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/ |
|
||||
| garage.kubernetesSkipCrd | bool | `false` | Set to true if you want to use k8s discovery but install the CRDs manually outside of the helm chart, for example if you operate at namespace level without cluster ressources |
|
||||
| garage.replicationFactor | string | `"3"` | Default to 3 replicas, see the replication_factor section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor |
|
||||
| garage.consistencyMode | string | `"consistent"` | Default to read-after-write consistency, see the consistency_mode section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode |
|
||||
| garage.metadataAutoSnapshotInterval | string | `""` | If this value is set, Garage will automatically take a snapshot of the metadata DB file at a regular interval and save it in the metadata directory. https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval |
|
||||
| garage.rpcBindAddr | string | `"[::]:3901"` | |
|
||||
| garage.rpcSecret | string | `""` | If not given, a random secret will be generated and stored in a Secret object |
|
||||
| garage.s3.api.region | string | `"garage"` | |
|
||||
| garage.s3.api.rootDomain | string | `".s3.garage.tld"` | |
|
||||
| garage.s3.web.index | string | `"index.html"` | |
|
||||
| garage.s3.web.rootDomain | string | `".web.garage.tld"` | |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| image.repository | string | `"dxflrs/amd64_garage"` | default to amd64 docker image |
|
||||
| image.tag | string | `""` | set the image tag, please prefer using the chart version and not this to avoid compatibility issues |
|
||||
| imagePullSecrets | list | `[]` | set if you need credentials to pull your custom image |
|
||||
| ingress.s3.api.annotations | object | `{}` | Rely _either_ on the className or the annotation below but not both! If you want to use the className, set className: "nginx" and replace "nginx" by an Ingress controller name, examples [here](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers). |
|
||||
| ingress.s3.api.enabled | bool | `false` | |
|
||||
| ingress.s3.api.hosts[0] | object | `{"host":"s3.garage.tld","paths":[{"path":"/","pathType":"Prefix"}]}` | garage S3 API endpoint, to be used with awscli for example |
|
||||
| ingress.s3.api.hosts[1] | object | `{"host":"*.s3.garage.tld","paths":[{"path":"/","pathType":"Prefix"}]}` | garage S3 API endpoint, DNS style bucket access |
|
||||
| ingress.s3.api.labels | object | `{}` | |
|
||||
| ingress.s3.api.tls | list | `[]` | |
|
||||
| ingress.s3.web.annotations | object | `{}` | Rely _either_ on the className or the annotation below but not both! If you want to use the className, set className: "nginx" and replace "nginx" by an Ingress controller name, examples [here](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers). |
|
||||
| ingress.s3.web.enabled | bool | `false` | |
|
||||
| ingress.s3.web.hosts[0] | object | `{"host":"*.web.garage.tld","paths":[{"path":"/","pathType":"Prefix"}]}` | wildcard website access with bucket name prefix |
|
||||
| ingress.s3.web.hosts[1] | object | `{"host":"mywebpage.example.com","paths":[{"path":"/","pathType":"Prefix"}]}` | specific bucket access with FQDN bucket |
|
||||
| ingress.s3.web.labels | object | `{}` | |
|
||||
| ingress.s3.web.tls | list | `[]` | |
|
||||
| initImage.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| initImage.repository | string | `"busybox"` | |
|
||||
| initImage.tag | string | `"stable"` | |
|
||||
| livenessProbe | object | `{}` | Specifies a livenessProbe |
|
||||
| monitoring.metrics.enabled | bool | `false` | If true, a service for monitoring is created with a prometheus.io/scrape annotation |
|
||||
| monitoring.metrics.serviceMonitor.enabled | bool | `false` | If true, a ServiceMonitor CRD is created for a prometheus operator https://github.com/coreos/prometheus-operator |
|
||||
| monitoring.metrics.serviceMonitor.interval | string | `"15s"` | |
|
||||
| monitoring.metrics.serviceMonitor.labels | object | `{}` | |
|
||||
| monitoring.metrics.serviceMonitor.path | string | `"/metrics"` | |
|
||||
| monitoring.metrics.serviceMonitor.relabelings | list | `[]` | |
|
||||
| monitoring.metrics.serviceMonitor.scheme | string | `"http"` | |
|
||||
| monitoring.metrics.serviceMonitor.scrapeTimeout | string | `"10s"` | |
|
||||
| monitoring.metrics.serviceMonitor.tlsConfig | object | `{}` | |
|
||||
| monitoring.tracing.sink | string | `""` | specify a sink endpoint for OpenTelemetry Traces, eg. `http://localhost:4317` |
|
||||
| nameOverride | string | `""` | |
|
||||
| nodeSelector | object | `{}` | |
|
||||
| persistence.data.hostPath | string | `"/var/lib/garage/data"` | |
|
||||
| persistence.data.size | string | `"100Mi"` | |
|
||||
| persistence.enabled | bool | `true` | |
|
||||
| persistence.meta.hostPath | string | `"/var/lib/garage/meta"` | |
|
||||
| persistence.meta.size | string | `"100Mi"` | |
|
||||
| podAnnotations | object | `{}` | additonal pod annotations |
|
||||
| podSecurityContext.fsGroup | int | `1000` | |
|
||||
| podSecurityContext.runAsGroup | int | `1000` | |
|
||||
| podSecurityContext.runAsNonRoot | bool | `true` | |
|
||||
| podSecurityContext.runAsUser | int | `1000` | |
|
||||
| readinessProbe | object | `{}` | Specifies a readinessProbe |
|
||||
| resources | object | `{}` | |
|
||||
| securityContext.capabilities | object | `{"drop":["ALL"]}` | The default security context is heavily restricted, feel free to tune it to your requirements |
|
||||
| securityContext.readOnlyRootFilesystem | bool | `true` | |
|
||||
| service.s3.api.port | int | `3900` | |
|
||||
| service.s3.web.port | int | `3902` | |
|
||||
| service.type | string | `"ClusterIP"` | You can rely on any service to expose your cluster - ClusterIP (+ Ingress) - NodePort (+ Ingress) - LoadBalancer |
|
||||
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
|
||||
| tolerations | list | `[]` | |
|
||||
|
||||
----------------------------------------------
|
||||
Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2)
|
||||
@@ -27,7 +27,7 @@ If release name contains chart name it will be used as a full name.
|
||||
Create the name of the rpc secret
|
||||
*/}}
|
||||
{{- define "garage.rpcSecretName" -}}
|
||||
{{- printf "%s-rpc-secret" (include "garage.fullname" .) -}}
|
||||
{{- .Values.garage.existingRpcSecret | default (printf "%s-rpc-secret" (include "garage.fullname" .)) -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
@@ -47,6 +47,9 @@ helm.sh/chart: {{ include "garage.chart" . }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- with .Values.commonLabels }}
|
||||
{{- toYaml . | nindent 0 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
|
||||
@@ -1,7 +1,62 @@
|
||||
{{- if not .Values.garage.existingConfigMap }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "garage.fullname" . }}-config
|
||||
data:
|
||||
garage.toml: |-
|
||||
{{- tpl (index (index .Values.garage) "garage.toml") $ | nindent 4 }}
|
||||
{{- if .Values.garage.garageTomlString }}
|
||||
{{- tpl (index (index .Values.garage) "garageTomlString") $ | nindent 4 }}
|
||||
{{- else }}
|
||||
metadata_dir = "/mnt/meta"
|
||||
data_dir = "/mnt/data"
|
||||
|
||||
db_engine = "{{ .Values.garage.dbEngine }}"
|
||||
|
||||
block_size = {{ .Values.garage.blockSize }}
|
||||
|
||||
replication_factor = {{ .Values.garage.replicationFactor }}
|
||||
consistency_mode = "{{ .Values.garage.consistencyMode }}"
|
||||
|
||||
compression_level = {{ .Values.garage.compressionLevel }}
|
||||
|
||||
{{- if .Values.garage.metadataAutoSnapshotInterval }}
|
||||
metadata_auto_snapshot_interval = {{ .Values.garage.metadataAutoSnapshotInterval | quote }}
|
||||
{{- end }}
|
||||
|
||||
rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}"
|
||||
# rpc_secret will be populated by the init container from a k8s secret object
|
||||
rpc_secret = "__RPC_SECRET_REPLACE__"
|
||||
|
||||
bootstrap_peers = [
|
||||
{{- range $index, $peer := .Values.garage.bootstrapPeers }}
|
||||
{{- if $index}}, {{ end }}{{ $peer | quote }}
|
||||
{{ end }}
|
||||
]
|
||||
|
||||
{{- if .Values.garage.additionalTopLevelConfig }}
|
||||
{{ .Values.garage.additionalTopLevelConfig | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
[kubernetes_discovery]
|
||||
namespace = "{{ .Release.Namespace }}"
|
||||
service_name = "{{ include "garage.fullname" . }}"
|
||||
skip_crd = {{ .Values.garage.kubernetesSkipCrd }}
|
||||
|
||||
[s3_api]
|
||||
s3_region = "{{ .Values.garage.s3.api.region }}"
|
||||
api_bind_addr = "[::]:3900"
|
||||
root_domain = "{{ .Values.garage.s3.api.rootDomain }}"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = "{{ .Values.garage.s3.web.rootDomain }}"
|
||||
index = "{{ .Values.garage.s3.web.index }}"
|
||||
|
||||
[admin]
|
||||
api_bind_addr = "[::]:3903"
|
||||
{{- if .Values.monitoring.tracing.sink }}
|
||||
trace_sink = "{{ .Values.monitoring.tracing.sink }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- if not .Values.garage.existingRpcSecret }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -12,3 +13,4 @@ data:
|
||||
{{- $prevRpcSecret := $prevSecretData.rpcSecret | default "" | b64dec }}
|
||||
{{/* Priority is: 1. from values, 2. previous value, 3. generate random */}}
|
||||
rpcSecret: {{ .Values.garage.rpcSecret | default $prevRpcSecret | default (include "jupyterhub.randHex" 64) | b64enc | quote }}
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{{- if eq .Values.deployment.kind "StatefulSet" -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "garage.fullname" . }}-headless
|
||||
labels:
|
||||
{{- include "garage.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
clusterIP: None
|
||||
ports:
|
||||
- port: {{ .Values.service.s3.api.port }}
|
||||
targetPort: 3900
|
||||
protocol: TCP
|
||||
name: s3-api
|
||||
- port: {{ .Values.service.s3.web.port }}
|
||||
targetPort: 3902
|
||||
protocol: TCP
|
||||
name: s3-web
|
||||
selector:
|
||||
{{- include "garage.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -4,6 +4,10 @@ metadata:
|
||||
name: {{ include "garage.fullname" . }}
|
||||
labels:
|
||||
{{- include "garage.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
@@ -37,4 +41,4 @@ spec:
|
||||
name: metrics
|
||||
selector:
|
||||
{{- include "garage.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -10,18 +10,18 @@ spec:
|
||||
{{- include "garage.selectorLabels" . | nindent 6 }}
|
||||
{{- if eq .Values.deployment.kind "StatefulSet" }}
|
||||
replicas: {{ .Values.deployment.replicaCount }}
|
||||
serviceName: {{ include "garage.fullname" . }}
|
||||
serviceName: {{ include "garage.fullname" . }}-headless
|
||||
podManagementPolicy: {{ .Values.deployment.podManagementPolicy }}
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "garage.selectorLabels" . | nindent 8 }}
|
||||
{{- include "garage.labels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
@@ -63,6 +63,10 @@ spec:
|
||||
name: web-api
|
||||
- containerPort: 3903
|
||||
name: admin
|
||||
{{- with .Values.environment }}
|
||||
env:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: meta
|
||||
mountPath: /mnt/meta
|
||||
@@ -71,15 +75,17 @@ spec:
|
||||
- name: etc
|
||||
mountPath: /etc/garage.toml
|
||||
subPath: garage.toml
|
||||
# TODO
|
||||
# livenessProbe:
|
||||
# httpGet:
|
||||
# path: /
|
||||
# port: 3900
|
||||
# readinessProbe:
|
||||
# httpGet:
|
||||
# path: /
|
||||
# port: 3900
|
||||
{{- with .Values.extraVolumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.livenessProbe }}
|
||||
livenessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.readinessProbe }}
|
||||
readinessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumes:
|
||||
@@ -105,6 +111,9 @@ spec:
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- with .Values.extraVolumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
|
||||
@@ -2,30 +2,49 @@
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# -- Additional labels to add to all resources created by this chart
|
||||
commonLabels: {}
|
||||
# app.kubernetes.io/part-of: storage
|
||||
# team: platform
|
||||
|
||||
# Garage configuration. These values go to garage.toml
|
||||
garage:
|
||||
# Can be changed for better performance on certain systems
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db-engine-since-v0-8-0
|
||||
# -- Can be changed for better performance on certain systems
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine
|
||||
dbEngine: "lmdb"
|
||||
|
||||
# Defaults is 1MB
|
||||
# -- Defaults is 1MB
|
||||
# An increase can result in better performance in certain scenarios
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block-size
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block_size
|
||||
blockSize: "1048576"
|
||||
|
||||
# Default to 3 replicas, see the replication_mode section at
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication-mode
|
||||
replicationMode: "3"
|
||||
# -- Default to 3 replicas, see the replication_factor section at
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor
|
||||
replicationFactor: "3"
|
||||
|
||||
# zstd compression level of stored blocks
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression-level
|
||||
# -- By default, enable read-after-write consistency guarantees, see the consistency_mode section at
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode
|
||||
consistencyMode: "consistent"
|
||||
|
||||
# -- zstd compression level of stored blocks
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression_level
|
||||
compressionLevel: "1"
|
||||
|
||||
# -- If this value is set, Garage will automatically take a snapshot of the metadata DB file at a regular interval and save it in the metadata directory.
|
||||
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval
|
||||
metadataAutoSnapshotInterval: ""
|
||||
|
||||
rpcBindAddr: "[::]:3901"
|
||||
# If not given, a random secret will be generated and stored in a Secret object
|
||||
# -- If not given, a random secret will be generated and stored in a Secret object
|
||||
rpcSecret: ""
|
||||
# This is not required if you use the integrated kubernetes discovery
|
||||
# -- If you want to provide an rpcSecret within an existing k8s secret,
|
||||
# specify the secret name here, and store the value under the secret key `rpcSecret`
|
||||
# the default secret will not be created
|
||||
existingRpcSecret: ""
|
||||
# -- This is not required if you use the integrated kubernetes discovery
|
||||
bootstrapPeers: []
|
||||
# -- Set to true if you want to use k8s discovery but install the CRDs manually outside
|
||||
# of the helm chart, for example if you operate at namespace level without cluster ressources
|
||||
kubernetesSkipCrd: false
|
||||
s3:
|
||||
api:
|
||||
@@ -34,47 +53,22 @@ garage:
|
||||
web:
|
||||
rootDomain: ".web.garage.tld"
|
||||
index: "index.html"
|
||||
# Template for the garage configuration
|
||||
# Values can be templated
|
||||
# ref: https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/
|
||||
garage.toml: |-
|
||||
metadata_dir = "/mnt/meta"
|
||||
data_dir = "/mnt/data"
|
||||
|
||||
db_engine = "{{ .Values.garage.dbEngine }}"
|
||||
# -- Additional configuration to append to garage.toml. Use a multi-line string for custom config.
|
||||
# Example:
|
||||
# additionalTopLevelConfig: |-
|
||||
# data_fsync = true
|
||||
additionalTopLevelConfig: ""
|
||||
|
||||
block_size = {{ .Values.garage.blockSize }}
|
||||
# -- if not empty string, allow using an existing ConfigMap for the garage.toml,
|
||||
# if set, ignores garage.toml
|
||||
existingConfigMap: ""
|
||||
|
||||
replication_mode = "{{ .Values.garage.replicationMode }}"
|
||||
|
||||
compression_level = {{ .Values.garage.compressionLevel }}
|
||||
|
||||
rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}"
|
||||
# rpc_secret will be populated by the init container from a k8s secret object
|
||||
rpc_secret = "__RPC_SECRET_REPLACE__"
|
||||
|
||||
bootstrap_peers = {{ .Values.garage.bootstrapPeers }}
|
||||
|
||||
[kubernetes_discovery]
|
||||
namespace = "{{ .Release.Namespace }}"
|
||||
service_name = "{{ include "garage.fullname" . }}"
|
||||
skip_crd = {{ .Values.garage.kubernetesSkipCrd }}
|
||||
|
||||
[s3_api]
|
||||
s3_region = "{{ .Values.garage.s3.api.region }}"
|
||||
api_bind_addr = "[::]:3900"
|
||||
root_domain = "{{ .Values.garage.s3.api.rootDomain }}"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = "{{ .Values.garage.s3.web.rootDomain }}"
|
||||
index = "{{ .Values.garage.s3.web.index }}"
|
||||
|
||||
[admin]
|
||||
api_bind_addr = "[::]:3903"
|
||||
{{- if .Values.monitoring.tracing.sink }}
|
||||
trace_sink = "{{ .Values.monitoring.tracing.sink }}"
|
||||
{{- end }}
|
||||
# -- String Template for the garage configuration
|
||||
# if set, ignores above values.
|
||||
# Values can be templated,
|
||||
# see https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/
|
||||
garageTomlString: ""
|
||||
|
||||
# Data persistence
|
||||
persistence:
|
||||
@@ -92,14 +86,18 @@ persistence:
|
||||
|
||||
# Deployment configuration
|
||||
deployment:
|
||||
# Switchable to DaemonSet
|
||||
# -- Switchable to DaemonSet
|
||||
kind: StatefulSet
|
||||
# Number of StatefulSet replicas/garage nodes to start
|
||||
# -- Number of StatefulSet replicas/garage nodes to start
|
||||
replicaCount: 3
|
||||
# -- If using statefulset, allow Parallel or OrderedReady (default)
|
||||
podManagementPolicy: OrderedReady
|
||||
|
||||
image:
|
||||
# -- default to amd64 docker image
|
||||
repository: dxflrs/amd64_garage
|
||||
# please prefer using the chart version and not this tag
|
||||
# -- set the image tag, please prefer using the chart version and not this
|
||||
# to avoid compatibility issues
|
||||
tag: ""
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
@@ -108,29 +106,32 @@ initImage:
|
||||
tag: stable
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# -- set if you need credentials to pull your custom image
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
# -- Specifies whether a service account should be created
|
||||
create: true
|
||||
# Annotations to add to the service account
|
||||
# -- Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# -- The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
# -- additonal pod annotations
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
runAsNonRoot: true
|
||||
|
||||
securityContext:
|
||||
# The default security context is heavily restricted
|
||||
# -- The default security context is heavily restricted,
|
||||
# feel free to tune it to your requirements
|
||||
capabilities:
|
||||
drop:
|
||||
@@ -138,11 +139,13 @@ securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
|
||||
service:
|
||||
# You can rely on any service to expose your cluster
|
||||
# -- You can rely on any service to expose your cluster
|
||||
# - ClusterIP (+ Ingress)
|
||||
# - NodePort (+ Ingress)
|
||||
# - LoadBalancer
|
||||
type: ClusterIP
|
||||
# -- Annotations to add to the service
|
||||
annotations: {}
|
||||
s3:
|
||||
api:
|
||||
port: 3900
|
||||
@@ -154,20 +157,23 @@ ingress:
|
||||
s3:
|
||||
api:
|
||||
enabled: false
|
||||
# Rely either on the className or the annotation below but not both
|
||||
# replace "nginx" by an Ingress controller
|
||||
# you can find examples here https://kubernetes.io/docs/concepts/services-networking/ingress-controllers
|
||||
# -- Rely _either_ on the className or the annotation below but not both!
|
||||
# If you want to use the className, set
|
||||
# className: "nginx"
|
||||
# and replace "nginx" by an Ingress controller name,
|
||||
# examples [here](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers).
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: "nginx"
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
labels: {}
|
||||
hosts:
|
||||
- host: "s3.garage.tld" # garage S3 API endpoint
|
||||
# -- garage S3 API endpoint, to be used with awscli for example
|
||||
- host: "s3.garage.tld"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
- host: "*.s3.garage.tld" # garage S3 API endpoint, DNS style bucket access
|
||||
# -- garage S3 API endpoint, DNS style bucket access
|
||||
- host: "*.s3.garage.tld"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
@@ -177,20 +183,23 @@ ingress:
|
||||
# - kubernetes.docker.internal
|
||||
web:
|
||||
enabled: false
|
||||
# Rely either on the className or the annotation below but not both
|
||||
# replace "nginx" by an Ingress controller
|
||||
# you can find examples here https://kubernetes.io/docs/concepts/services-networking/ingress-controllers
|
||||
# -- Rely _either_ on the className or the annotation below but not both!
|
||||
# If you want to use the className, set
|
||||
# className: "nginx"
|
||||
# and replace "nginx" by an Ingress controller name,
|
||||
# examples [here](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers).
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
labels: {}
|
||||
hosts:
|
||||
- host: "*.web.garage.tld" # wildcard website access with bucket name prefix
|
||||
# -- wildcard website access with bucket name prefix
|
||||
- host: "*.web.garage.tld"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
- host: "mywebpage.example.com" # specific bucket access with FQDN bucket
|
||||
# -- specific bucket access with FQDN bucket
|
||||
- host: "mywebpage.example.com"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
@@ -208,18 +217,39 @@ resources: {}
|
||||
# cpu: 100m
|
||||
# memory: 512Mi
|
||||
|
||||
# -- Specifies a livenessProbe
|
||||
livenessProbe: {}
|
||||
#httpGet:
|
||||
# path: /health
|
||||
# port: 3903
|
||||
#initialDelaySeconds: 5
|
||||
#periodSeconds: 30
|
||||
# -- Specifies a readinessProbe
|
||||
readinessProbe: {}
|
||||
#httpGet:
|
||||
# path: /health
|
||||
# port: 3903
|
||||
#initialDelaySeconds: 5
|
||||
#periodSeconds: 30
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
environment: {}
|
||||
|
||||
extraVolumes: {}
|
||||
|
||||
extraVolumeMounts: {}
|
||||
|
||||
monitoring:
|
||||
metrics:
|
||||
# If true, a service for monitoring is created with a prometheus.io/scrape annotation
|
||||
# -- If true, a service for monitoring is created with a prometheus.io/scrape annotation
|
||||
enabled: false
|
||||
serviceMonitor:
|
||||
# If true, a ServiceMonitor CRD is created for a prometheus operator
|
||||
# -- If true, a ServiceMonitor CRD is created for a prometheus operator
|
||||
# https://github.com/coreos/prometheus-operator
|
||||
enabled: false
|
||||
path: /metrics
|
||||
@@ -231,4 +261,5 @@ monitoring:
|
||||
scrapeTimeout: 10s
|
||||
relabelings: []
|
||||
tracing:
|
||||
# -- specify a sink endpoint for OpenTelemetry Traces, eg. `http://localhost:4317`
|
||||
sink: ""
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"rpc_bind_addr = \"0.0.0.0:3901\"\n"
|
||||
"rpc_public_addr = \"" node ":3901\"\n"
|
||||
"db_engine = \"lmdb\"\n"
|
||||
"replication_mode = \"3\"\n"
|
||||
"replication_factor = 3\n"
|
||||
"data_dir = \"" data-dir "\"\n"
|
||||
"metadata_dir = \"" meta-dir "\"\n"
|
||||
"[s3_api]\n"
|
||||
|
||||
@@ -8,7 +8,7 @@ data:
|
||||
metadata_dir = "/tmp/meta"
|
||||
data_dir = "/tmp/data"
|
||||
|
||||
replication_mode = "3"
|
||||
replication_factor = 3
|
||||
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
rpc_secret = "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: garagenodes.deuxfleurs.fr
|
||||
spec:
|
||||
conversion:
|
||||
strategy: None
|
||||
group: deuxfleurs.fr
|
||||
names:
|
||||
kind: GarageNode
|
||||
listKind: GarageNodeList
|
||||
plural: garagenodes
|
||||
singular: garagenode
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Auto-generated derived type for Node via `CustomResource`
|
||||
properties:
|
||||
spec:
|
||||
properties:
|
||||
address:
|
||||
format: ip
|
||||
type: string
|
||||
hostname:
|
||||
type: string
|
||||
port:
|
||||
format: uint16
|
||||
minimum: 0
|
||||
type: integer
|
||||
required:
|
||||
- address
|
||||
- hostname
|
||||
- port
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
title: GarageNode
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources: {}
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- garagenodes.deuxfleurs.fr.yaml
|
||||
@@ -7,7 +7,12 @@ if [ "$#" -ne 1 ]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if file $1 | grep 'dynamically linked' 2>&1; then
|
||||
if [ ! -x "$1" ]; then
|
||||
echo "[fail] $1 does not exist or is not an executable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if file "$1" | grep 'dynamically linked' 2>&1; then
|
||||
echo "[fail] $1 is dynamic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -694,32 +694,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"__systemRef": "hideSeriesFrom",
|
||||
"matcher": {
|
||||
"id": "byNames",
|
||||
"options": {
|
||||
"mode": "exclude",
|
||||
"names": [
|
||||
"10.83.2.3:3903"
|
||||
],
|
||||
"prefix": "All except:",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hideFrom",
|
||||
"value": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
|
||||
@@ -112,6 +112,23 @@ if [ -z "$SKIP_S3CMD" ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# BOTO3
|
||||
if [ -z "$SKIP_BOTO3" ]; then
|
||||
echo "🛠️ Testing with boto3 for STREAMING-UNSIGNED-PAYLOAD-TRAILER"
|
||||
source ${SCRIPT_FOLDER}/dev-env-aws.sh
|
||||
AWS_ENDPOINT_URL=https://localhost:4443 python <<EOF
|
||||
import boto3
|
||||
client = boto3.client('s3', verify=False)
|
||||
print("Put&delete hello world object")
|
||||
client.put_object(Body=b'hello world', Bucket='eprouvette', Key='test.s3.txt')
|
||||
client.delete_object(Bucket='eprouvette', Key='test.s3.txt')
|
||||
print("Put&delete big object")
|
||||
client.upload_file("/tmp/garage.3.rnd", 'eprouvette', 'garage.3.rnd')
|
||||
client.delete_object(Bucket='eprouvette', Key='garage.3.rnd')
|
||||
print("OK!")
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Minio Client
|
||||
if [ -z "$SKIP_MC" ]; then
|
||||
echo "🛠️ Testing with mc (minio client)"
|
||||
|
||||
@@ -24,9 +24,17 @@ echo "============= insert data into old version cluster ================="
|
||||
export GARAGE_BIN=/tmp/old_garage
|
||||
if echo $OLD_VERSION | grep 'v0\.8\.'; then
|
||||
echo "Detected Garage v0.8.x"
|
||||
export GARAGE_08=1
|
||||
export GARAGE_OLDVER=v08
|
||||
elif (echo $OLD_VERSION | grep 'v0\.9\.') || (echo $OLD_VERSION | grep 'v1\.'); then
|
||||
echo "Detected Garage v0.9.x / v1.x"
|
||||
export GARAGE_OLDVER=v1
|
||||
fi
|
||||
|
||||
if echo $OLD_VERSION | grep 'v1\.'; then
|
||||
DO_SSEC_TEST=1
|
||||
fi
|
||||
SSEC_KEY="u8zCfnEyt5Imo/krN+sxA1DQXxLWtPJavU6T6gOVj1Y="
|
||||
|
||||
echo "⏳ Setup cluster using old version"
|
||||
$GARAGE_BIN --version
|
||||
${SCRIPT_FOLDER}/dev-clean.sh
|
||||
@@ -37,7 +45,23 @@ ${SCRIPT_FOLDER}/dev-bucket.sh
|
||||
|
||||
echo "🛠️ Inserting data in old cluster"
|
||||
source ${SCRIPT_FOLDER}/dev-env-rclone.sh
|
||||
rclone copy "${SCRIPT_FOLDER}/../.git/" garage:eprouvette/test_dotgit --stats=1s --stats-log-level=NOTICE --stats-one-line
|
||||
rclone copy "${SCRIPT_FOLDER}/../.git/" garage:eprouvette/test_dotgit \
|
||||
--stats=1s --stats-log-level=NOTICE --stats-one-line
|
||||
|
||||
if [ "$DO_SSEC_TEST" = "1" ]; then
|
||||
# upload small file (should be single part)
|
||||
rclone copy "${SCRIPT_FOLDER}/test-upgrade.sh" garage:eprouvette/test-ssec \
|
||||
--s3-sse-customer-algorithm AES256 \
|
||||
--s3-sse-customer-key-base64 "$SSEC_KEY" \
|
||||
--stats=1s --stats-log-level=NOTICE --stats-one-line
|
||||
# do a multipart upload
|
||||
dd if=/dev/urandom of=/tmp/randfile-for-upgrade bs=5M count=5
|
||||
rclone copy "/tmp/randfile-for-upgrade" garage:eprouvette/test-ssec \
|
||||
--s3-chunk-size 5M \
|
||||
--s3-sse-customer-algorithm AES256 \
|
||||
--s3-sse-customer-key-base64 "$SSEC_KEY" \
|
||||
--stats=1s --stats-log-level=NOTICE --stats-one-line
|
||||
fi
|
||||
|
||||
echo "🏁 Stopping old cluster"
|
||||
killall -INT old_garage
|
||||
@@ -47,7 +71,7 @@ killall -9 old_garage || true
|
||||
echo "🏁 Removing old garage version"
|
||||
rm -rv $GARAGE_BIN
|
||||
export -n GARAGE_BIN
|
||||
export -n GARAGE_08
|
||||
export -n GARAGE_OLDVER
|
||||
|
||||
echo "================ read data from new cluster ==================="
|
||||
|
||||
@@ -60,7 +84,8 @@ ${SCRIPT_FOLDER}/dev-cluster.sh >> /tmp/garage.log 2>&1 &
|
||||
sleep 3
|
||||
|
||||
echo "🛠️ Retrieving data from old cluster"
|
||||
rclone copy garage:eprouvette/test_dotgit /tmp/test_dotgit --stats=1s --stats-log-level=NOTICE --stats-one-line --fast-list
|
||||
rclone copy garage:eprouvette/test_dotgit /tmp/test_dotgit \
|
||||
--stats=1s --stats-log-level=NOTICE --stats-one-line --fast-list
|
||||
|
||||
if ! diff <(find "${SCRIPT_FOLDER}/../.git" -type f | xargs md5sum | cut -d ' ' -f 1 | sort) <(find /tmp/test_dotgit -type f | xargs md5sum | cut -d ' ' -f 1 | sort); then
|
||||
echo "TEST FAILURE: directories are different"
|
||||
@@ -68,6 +93,23 @@ if ! diff <(find "${SCRIPT_FOLDER}/../.git" -type f | xargs md5sum | cut -d ' '
|
||||
fi
|
||||
rm -r /tmp/test_dotgit
|
||||
|
||||
if [ "$DO_SSEC_TEST" = "1" ]; then
|
||||
rclone copy garage:eprouvette/test-ssec /tmp/test_ssec_out \
|
||||
--s3-sse-customer-algorithm AES256 \
|
||||
--s3-sse-customer-key-base64 "$SSEC_KEY" \
|
||||
--stats=1s --stats-log-level=NOTICE --stats-one-line
|
||||
if ! diff "/tmp/test_ssec_out/test-upgrade.sh" "${SCRIPT_FOLDER}/test-upgrade.sh"; then
|
||||
echo "SSEC-FAILURE (small file)"
|
||||
exit 1
|
||||
fi
|
||||
if ! diff "/tmp/test_ssec_out/randfile-for-upgrade" "/tmp/randfile-for-upgrade"; then
|
||||
echo "SSEC-FAILURE (big file)"
|
||||
exit 1
|
||||
fi
|
||||
rm -r /tmp/test_ssec_out
|
||||
rm /tmp/randfile-for-upgrade
|
||||
fi
|
||||
|
||||
echo "🏁 Teardown"
|
||||
rm -rf /tmp/garage-{data,meta}-*
|
||||
rm -rf /tmp/config.*.toml
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
with import ./nix/common.nix;
|
||||
|
||||
let
|
||||
pkgs = import pkgsSrc {
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
winscp = (import ./nix/winscp.nix) pkgs;
|
||||
@@ -11,6 +11,7 @@ in
|
||||
{
|
||||
# --- Dev shell inherited from flake.nix ---
|
||||
devShell = devShells.default;
|
||||
devShellFull = devShells.full;
|
||||
|
||||
# --- Continuous integration shell ---
|
||||
# The shell used for all CI jobs (along with devShell)
|
||||
@@ -25,6 +26,8 @@ in
|
||||
s3cmd
|
||||
minio-client
|
||||
rclone
|
||||
(python313.withPackages (ps: [ ps.boto3 ]))
|
||||
|
||||
socat
|
||||
psmisc
|
||||
which
|
||||
@@ -33,12 +36,15 @@ in
|
||||
jq
|
||||
];
|
||||
shellHook = ''
|
||||
export AWS_REQUEST_CHECKSUM_CALCULATION='when_required'
|
||||
|
||||
function to_s3 {
|
||||
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED \
|
||||
aws \
|
||||
--endpoint-url https://garage.deuxfleurs.fr \
|
||||
--region garage \
|
||||
s3 cp \
|
||||
./result-bin/bin/garage \
|
||||
./result/bin/garage \
|
||||
s3://garagehq.deuxfleurs.fr/_releases/''${CI_COMMIT_TAG:-$CI_COMMIT_SHA}/''${TARGET}/garage
|
||||
}
|
||||
|
||||
@@ -90,6 +96,7 @@ in
|
||||
|
||||
nix-build nix/build_index.nix
|
||||
|
||||
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED \
|
||||
aws \
|
||||
--endpoint-url https://garage.deuxfleurs.fr \
|
||||
--region garage \
|
||||
@@ -97,6 +104,7 @@ in
|
||||
result/share/_releases.json \
|
||||
s3://garagehq.deuxfleurs.fr/
|
||||
|
||||
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED \
|
||||
aws \
|
||||
--endpoint-url https://garage.deuxfleurs.fr \
|
||||
--region garage \
|
||||
@@ -114,7 +122,7 @@ in
|
||||
shellHook = ''
|
||||
function refresh_cache {
|
||||
pass show deuxfleurs/nix_priv_key > /tmp/nix-signing-key.sec
|
||||
for attr in clippy.amd64 test.amd64 pkgs.{amd64,i386,arm,arm64}.release; do
|
||||
for attr in pkgs.amd64.debug test.amd64 pkgs.{amd64,i386,arm,arm64}.release; do
|
||||
echo "Updating cache for ''${attr}"
|
||||
nix copy -j8 \
|
||||
--to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/tmp/nix-signing-key.sec' \
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
[package]
|
||||
name = "garage_api_admin"
|
||||
version = "2.2.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
description = "Admin API server crate for the Garage object store"
|
||||
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
|
||||
readme = "../../../README.md"
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
format_table.workspace = true
|
||||
garage_model.workspace = true
|
||||
garage_block.workspace = true
|
||||
garage_table.workspace = true
|
||||
garage_util.workspace = true
|
||||
garage_rpc.workspace = true
|
||||
garage_api_common.workspace = true
|
||||
|
||||
argon2.workspace = true
|
||||
async-trait.workspace = true
|
||||
bytesize.workspace = true
|
||||
chrono.workspace = true
|
||||
thiserror.workspace = true
|
||||
hex.workspace = true
|
||||
paste.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
futures.workspace = true
|
||||
tokio.workspace = true
|
||||
http.workspace = true
|
||||
hyper = { workspace = true, default-features = false, features = ["server", "http1"] }
|
||||
url.workspace = true
|
||||
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
|
||||
opentelemetry.workspace = true
|
||||
opentelemetry-prometheus = { workspace = true, optional = true }
|
||||
prometheus = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
metrics = [ "opentelemetry-prometheus", "prometheus" ]
|
||||
k2v = [ "garage_model/k2v" ]
|
||||
@@ -0,0 +1,292 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use garage_table::*;
|
||||
use garage_util::time::now_msec;
|
||||
|
||||
use garage_model::admin_token_table::*;
|
||||
use garage_model::garage::Garage;
|
||||
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
use crate::{Admin, RequestHandler};
|
||||
|
||||
impl RequestHandler for ListAdminTokensRequest {
|
||||
type Response = ListAdminTokensResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<ListAdminTokensResponse, Error> {
|
||||
let now = now_msec();
|
||||
|
||||
let mut res = garage
|
||||
.admin_token_table
|
||||
.get_range(
|
||||
&EmptyKey,
|
||||
None,
|
||||
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
|
||||
10000,
|
||||
EnumerationOrder::Forward,
|
||||
)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|t| admin_token_info_results(t, now))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if garage.config.admin.metrics_token.is_some() {
|
||||
res.insert(
|
||||
0,
|
||||
GetAdminTokenInfoResponse {
|
||||
id: None,
|
||||
created: None,
|
||||
name: "metrics_token (from daemon configuration)".into(),
|
||||
expiration: None,
|
||||
expired: false,
|
||||
scope: vec!["Metrics".into()],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if garage.config.admin.admin_token.is_some() {
|
||||
res.insert(
|
||||
0,
|
||||
GetAdminTokenInfoResponse {
|
||||
id: None,
|
||||
created: None,
|
||||
name: "admin_token (from daemon configuration)".into(),
|
||||
expiration: None,
|
||||
expired: false,
|
||||
scope: vec!["*".into()],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(ListAdminTokensResponse(res))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for GetAdminTokenInfoRequest {
|
||||
type Response = GetAdminTokenInfoResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<GetAdminTokenInfoResponse, Error> {
|
||||
let token = match (self.id, self.search) {
|
||||
(Some(id), None) => get_existing_admin_token(garage, &id).await?,
|
||||
(None, Some(search)) => {
|
||||
let candidates = garage
|
||||
.admin_token_table
|
||||
.get_range(
|
||||
&EmptyKey,
|
||||
None,
|
||||
Some(KeyFilter::MatchesAndNotDeleted(search.to_string())),
|
||||
10,
|
||||
EnumerationOrder::Forward,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
if candidates.len() != 1 {
|
||||
return Err(Error::bad_request(format!(
|
||||
"{} matching admin tokens",
|
||||
candidates.len()
|
||||
)));
|
||||
}
|
||||
candidates.into_iter().next().unwrap()
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::bad_request(
|
||||
"Either id or search must be provided (but not both)",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(admin_token_info_results(&token, now_msec()))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for CreateAdminTokenRequest {
|
||||
type Response = CreateAdminTokenResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<CreateAdminTokenResponse, Error> {
|
||||
let (mut token, secret) = if self.0.name.is_some() {
|
||||
AdminApiToken::new("")
|
||||
} else {
|
||||
AdminApiToken::new(&format!("token_{}", Utc::now().format("%Y%m%d_%H%M")))
|
||||
};
|
||||
|
||||
apply_token_updates(&mut token, self.0)?;
|
||||
|
||||
garage.admin_token_table.insert(&token).await?;
|
||||
|
||||
Ok(CreateAdminTokenResponse {
|
||||
secret_token: secret,
|
||||
info: admin_token_info_results(&token, now_msec()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for UpdateAdminTokenRequest {
|
||||
type Response = UpdateAdminTokenResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<UpdateAdminTokenResponse, Error> {
|
||||
let mut token = get_existing_admin_token(&garage, &self.id).await?;
|
||||
|
||||
apply_token_updates(&mut token, self.body)?;
|
||||
|
||||
garage.admin_token_table.insert(&token).await?;
|
||||
|
||||
Ok(UpdateAdminTokenResponse(admin_token_info_results(
|
||||
&token,
|
||||
now_msec(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for DeleteAdminTokenRequest {
|
||||
type Response = DeleteAdminTokenResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<DeleteAdminTokenResponse, Error> {
|
||||
let token = get_existing_admin_token(&garage, &self.id).await?;
|
||||
|
||||
garage
|
||||
.admin_token_table
|
||||
.insert(&AdminApiToken::delete(token.prefix))
|
||||
.await?;
|
||||
|
||||
Ok(DeleteAdminTokenResponse)
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for GetCurrentAdminTokenInfoRequest {
|
||||
type Response = GetCurrentAdminTokenInfoResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<GetCurrentAdminTokenInfoResponse, Error> {
|
||||
let now = now_msec();
|
||||
|
||||
if garage
|
||||
.config
|
||||
.admin
|
||||
.metrics_token
|
||||
.as_ref()
|
||||
.is_some_and(|s| s == &self.admin_token)
|
||||
{
|
||||
return Ok(GetCurrentAdminTokenInfoResponse(
|
||||
GetAdminTokenInfoResponse {
|
||||
id: None,
|
||||
created: None,
|
||||
name: "metrics_token (from daemon configuration)".into(),
|
||||
expiration: None,
|
||||
expired: false,
|
||||
scope: vec!["Metrics".into()],
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if garage
|
||||
.config
|
||||
.admin
|
||||
.admin_token
|
||||
.as_ref()
|
||||
.is_some_and(|s| s == &self.admin_token)
|
||||
{
|
||||
return Ok(GetCurrentAdminTokenInfoResponse(
|
||||
GetAdminTokenInfoResponse {
|
||||
id: None,
|
||||
created: None,
|
||||
name: "admin_token (from daemon configuration)".into(),
|
||||
expiration: None,
|
||||
expired: false,
|
||||
scope: vec!["*".into()],
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let (prefix, _) = self.admin_token.split_once('.').unwrap();
|
||||
let token = get_existing_admin_token(&garage, &prefix.to_string()).await?;
|
||||
|
||||
Ok(GetCurrentAdminTokenInfoResponse(admin_token_info_results(
|
||||
&token, now,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInfoResponse {
|
||||
let params = token.params().unwrap();
|
||||
|
||||
GetAdminTokenInfoResponse {
|
||||
id: Some(token.prefix.clone()),
|
||||
created: Some(
|
||||
DateTime::from_timestamp_millis(params.created as i64)
|
||||
.expect("invalid timestamp stored in db"),
|
||||
),
|
||||
name: params.name.get().to_string(),
|
||||
expiration: params.expiration.get().map(|x| {
|
||||
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expired: params.is_expired(now),
|
||||
scope: params.scope.get().0.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_existing_admin_token(garage: &Garage, id: &String) -> Result<AdminApiToken, Error> {
|
||||
garage
|
||||
.admin_token_table
|
||||
.get(&EmptyKey, id)
|
||||
.await?
|
||||
.filter(|k| !k.state.is_deleted())
|
||||
.ok_or_else(|| Error::NoSuchAdminToken(id.to_string()))
|
||||
}
|
||||
|
||||
fn apply_token_updates(
|
||||
token: &mut AdminApiToken,
|
||||
updates: UpdateAdminTokenRequestBody,
|
||||
) -> Result<(), Error> {
|
||||
if updates.never_expires && updates.expiration.is_some() {
|
||||
return Err(Error::bad_request(
|
||||
"cannot specify `expiration` and `never_expires`",
|
||||
));
|
||||
}
|
||||
|
||||
let params = token.params_mut().unwrap();
|
||||
|
||||
if let Some(name) = updates.name {
|
||||
params.name.update(name);
|
||||
}
|
||||
if let Some(expiration) = updates.expiration {
|
||||
params
|
||||
.expiration
|
||||
.update(Some(expiration.timestamp_millis() as u64));
|
||||
}
|
||||
if updates.never_expires {
|
||||
params.expiration.update(None);
|
||||
}
|
||||
if let Some(scope) = updates.scope {
|
||||
params.scope.update(AdminApiTokenScope(scope));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,335 +1,237 @@
|
||||
use std::collections::HashMap;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use argon2::password_hash::PasswordHash;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
|
||||
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
|
||||
use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION};
|
||||
use hyper::{body::Incoming as IncomingBody, Request, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::watch;
|
||||
|
||||
use opentelemetry::trace::SpanRef;
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
use opentelemetry_prometheus::PrometheusExporter;
|
||||
#[cfg(feature = "metrics")]
|
||||
use prometheus::{Encoder, TextEncoder};
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_rpc::system::ClusterHealthStatus;
|
||||
use garage_rpc::{Endpoint as RpcEndpoint, *};
|
||||
use garage_table::EmptyKey;
|
||||
use garage_util::background::BackgroundRunner;
|
||||
use garage_util::data::Uuid;
|
||||
use garage_util::error::Error as GarageError;
|
||||
use garage_util::socket_address::UnixOrTCPSocketAddress;
|
||||
use garage_util::time::now_msec;
|
||||
|
||||
use crate::generic_server::*;
|
||||
use garage_api_common::generic_server::*;
|
||||
use garage_api_common::helpers::*;
|
||||
|
||||
use crate::admin::bucket::*;
|
||||
use crate::admin::cluster::*;
|
||||
use crate::admin::error::*;
|
||||
use crate::admin::key::*;
|
||||
use crate::admin::router_v0;
|
||||
use crate::admin::router_v1::{Authorization, Endpoint};
|
||||
use crate::helpers::*;
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
use crate::router_v0;
|
||||
use crate::router_v1;
|
||||
use crate::Authorization;
|
||||
use crate::RequestHandler;
|
||||
|
||||
// ---- FOR RPC ----
|
||||
|
||||
pub const ADMIN_RPC_PATH: &str = "garage_api/admin/rpc.rs/Rpc";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum AdminRpc {
|
||||
Proxy(AdminApiRequest),
|
||||
Internal(LocalAdminApiRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum AdminRpcResponse {
|
||||
ProxyApiOkResponse(TaggedAdminApiResponse),
|
||||
InternalApiOkResponse(LocalAdminApiResponse),
|
||||
ApiErrorResponse {
|
||||
http_code: u16,
|
||||
error_code: String,
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Rpc for AdminRpc {
|
||||
type Response = Result<AdminRpcResponse, GarageError>;
|
||||
}
|
||||
|
||||
impl EndpointHandler<AdminRpc> for AdminApiServer {
|
||||
async fn handle(
|
||||
self: &Arc<Self>,
|
||||
message: &AdminRpc,
|
||||
_from: NodeID,
|
||||
) -> Result<AdminRpcResponse, GarageError> {
|
||||
match message {
|
||||
AdminRpc::Proxy(req) => {
|
||||
info!("Proxied admin API request: {}", req.name());
|
||||
let res = req.clone().handle(&self.garage, &self).await;
|
||||
match res {
|
||||
Ok(res) => Ok(AdminRpcResponse::ProxyApiOkResponse(res.tagged())),
|
||||
Err(e) => Ok(AdminRpcResponse::ApiErrorResponse {
|
||||
http_code: e.http_status_code().as_u16(),
|
||||
error_code: e.code().to_string(),
|
||||
message: e.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
AdminRpc::Internal(req) => {
|
||||
info!("Internal admin API request: {}", req.name());
|
||||
let res = req.clone().handle(&self.garage, &self).await;
|
||||
match res {
|
||||
Ok(res) => Ok(AdminRpcResponse::InternalApiOkResponse(res)),
|
||||
Err(e) => Ok(AdminRpcResponse::ApiErrorResponse {
|
||||
http_code: e.http_status_code().as_u16(),
|
||||
error_code: e.code().to_string(),
|
||||
message: e.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- FOR HTTP ----
|
||||
|
||||
pub type ResBody = BoxBody<Error>;
|
||||
|
||||
pub struct AdminApiServer {
|
||||
garage: Arc<Garage>,
|
||||
#[cfg(feature = "metrics")]
|
||||
exporter: PrometheusExporter,
|
||||
pub(crate) exporter: PrometheusExporter,
|
||||
metrics_token: Option<String>,
|
||||
metrics_require_token: bool,
|
||||
admin_token: Option<String>,
|
||||
pub(crate) background: Arc<BackgroundRunner>,
|
||||
pub(crate) endpoint: Arc<RpcEndpoint<AdminRpc, Self>>,
|
||||
}
|
||||
|
||||
pub enum HttpEndpoint {
|
||||
Old(router_v1::Endpoint),
|
||||
New(String),
|
||||
}
|
||||
|
||||
impl AdminApiServer {
|
||||
pub fn new(
|
||||
garage: Arc<Garage>,
|
||||
background: Arc<BackgroundRunner>,
|
||||
#[cfg(feature = "metrics")] exporter: PrometheusExporter,
|
||||
) -> Self {
|
||||
) -> Arc<Self> {
|
||||
let cfg = &garage.config.admin;
|
||||
let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token);
|
||||
let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token);
|
||||
Self {
|
||||
let metrics_require_token = cfg.metrics_require_token;
|
||||
|
||||
let endpoint = garage.system.netapp.endpoint(ADMIN_RPC_PATH.into());
|
||||
let admin = Arc::new(Self {
|
||||
garage,
|
||||
#[cfg(feature = "metrics")]
|
||||
exporter,
|
||||
metrics_token,
|
||||
metrics_require_token,
|
||||
admin_token,
|
||||
}
|
||||
background,
|
||||
endpoint,
|
||||
});
|
||||
admin.endpoint.set_handler(admin.clone());
|
||||
admin
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
self,
|
||||
self: Arc<Self>,
|
||||
bind_addr: UnixOrTCPSocketAddress,
|
||||
must_exit: watch::Receiver<bool>,
|
||||
) -> Result<(), GarageError> {
|
||||
let region = self.garage.config.s3_api.s3_region.clone();
|
||||
ApiServer::new(region, self)
|
||||
ApiServer::new(region, ArcAdminApiServer(self))
|
||||
.run_server(bind_addr, Some(0o220), must_exit)
|
||||
.await
|
||||
}
|
||||
|
||||
fn handle_options(&self, _req: &Request<IncomingBody>) -> Result<Response<ResBody>, Error> {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.header(ALLOW, "OPTIONS, GET, POST")
|
||||
.header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST")
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.body(empty_body())?)
|
||||
}
|
||||
|
||||
async fn handle_check_domain(
|
||||
async fn handle_http_api(
|
||||
&self,
|
||||
req: Request<IncomingBody>,
|
||||
endpoint: HttpEndpoint,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let query_params: HashMap<String, String> = req
|
||||
.uri()
|
||||
.query()
|
||||
.map(|v| {
|
||||
url::form_urlencoded::parse(v.as_bytes())
|
||||
.into_owned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(HashMap::new);
|
||||
let auth_header = req.headers().get(AUTHORIZATION).cloned();
|
||||
|
||||
let has_domain_key = query_params.contains_key("domain");
|
||||
|
||||
if !has_domain_key {
|
||||
return Err(Error::bad_request("No domain query string found"));
|
||||
}
|
||||
|
||||
let domain = query_params
|
||||
.get("domain")
|
||||
.ok_or_internal_error("Could not parse domain query string")?;
|
||||
|
||||
if self.check_domain(domain).await? {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(string_body(format!(
|
||||
"Domain '{domain}' is managed by Garage"
|
||||
)))?)
|
||||
} else {
|
||||
Err(Error::bad_request(format!(
|
||||
"Domain '{domain}' is not managed by Garage"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_domain(&self, domain: &str) -> Result<bool, Error> {
|
||||
// Resolve bucket from domain name, inferring if the website must be activated for the
|
||||
// domain to be valid.
|
||||
let (bucket_name, must_check_website) = if let Some(bname) = self
|
||||
.garage
|
||||
.config
|
||||
.s3_api
|
||||
.root_domain
|
||||
.as_ref()
|
||||
.and_then(|rd| host_to_bucket(domain, rd))
|
||||
{
|
||||
(bname.to_string(), false)
|
||||
} else if let Some(bname) = self
|
||||
.garage
|
||||
.config
|
||||
.s3_web
|
||||
.as_ref()
|
||||
.and_then(|sw| host_to_bucket(domain, sw.root_domain.as_str()))
|
||||
{
|
||||
(bname.to_string(), true)
|
||||
} else {
|
||||
(domain.to_string(), true)
|
||||
let request = match endpoint {
|
||||
HttpEndpoint::Old(endpoint_v1) => AdminApiRequest::from_v1(endpoint_v1, req).await?,
|
||||
HttpEndpoint::New(_) => AdminApiRequest::from_request(req).await?,
|
||||
};
|
||||
|
||||
let bucket_id = match self
|
||||
.garage
|
||||
.bucket_helper()
|
||||
.resolve_global_bucket_name(&bucket_name)
|
||||
.await?
|
||||
{
|
||||
Some(bucket_id) => bucket_id,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
if !must_check_website {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let bucket = self
|
||||
.garage
|
||||
.bucket_helper()
|
||||
.get_existing_bucket(bucket_id)
|
||||
.await?;
|
||||
|
||||
let bucket_state = bucket.state.as_option().unwrap();
|
||||
let bucket_website_config = bucket_state.website_config.get();
|
||||
|
||||
match bucket_website_config {
|
||||
Some(_v) => Ok(true),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_health(&self) -> Result<Response<ResBody>, Error> {
|
||||
let health = self.garage.system.health();
|
||||
|
||||
let (status, status_str) = match health.status {
|
||||
ClusterHealthStatus::Healthy => (StatusCode::OK, "Garage is fully operational"),
|
||||
ClusterHealthStatus::Degraded => (
|
||||
StatusCode::OK,
|
||||
"Garage is operational but some storage nodes are unavailable",
|
||||
),
|
||||
ClusterHealthStatus::Unavailable => (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"Quorum is not available for some/all partitions, reads and writes will fail",
|
||||
let (global_token_hash, token_required) = match request.authorization_type() {
|
||||
Authorization::None => (None, false),
|
||||
Authorization::MetricsToken => (
|
||||
self.metrics_token.as_deref(),
|
||||
self.metrics_token.is_some() || self.metrics_require_token,
|
||||
),
|
||||
Authorization::AdminToken => (self.admin_token.as_deref(), true),
|
||||
};
|
||||
let status_str = format!(
|
||||
"{}\nConsult the full health check API endpoint at /v1/health for more details\n",
|
||||
status_str
|
||||
);
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(status)
|
||||
.header(http::header::CONTENT_TYPE, "text/plain")
|
||||
.body(string_body(status_str))?)
|
||||
}
|
||||
|
||||
fn handle_metrics(&self) -> Result<Response<ResBody>, Error> {
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
use opentelemetry::trace::Tracer;
|
||||
|
||||
let mut buffer = vec![];
|
||||
let encoder = TextEncoder::new();
|
||||
|
||||
let tracer = opentelemetry::global::tracer("garage");
|
||||
let metric_families = tracer.in_span("admin/gather_metrics", |_| {
|
||||
self.exporter.registry().gather()
|
||||
});
|
||||
|
||||
encoder
|
||||
.encode(&metric_families, &mut buffer)
|
||||
.ok_or_internal_error("Could not serialize metrics")?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(http::header::CONTENT_TYPE, encoder.format_type())
|
||||
.body(bytes_body(buffer.into()))?)
|
||||
if token_required {
|
||||
verify_authorization(&self.garage, global_token_hash, auth_header, request.name())?;
|
||||
}
|
||||
|
||||
match request {
|
||||
AdminApiRequest::Options(req) => req.handle(&self.garage, &self).await,
|
||||
AdminApiRequest::CheckDomain(req) => req.handle(&self.garage, &self).await,
|
||||
AdminApiRequest::Health(req) => req.handle(&self.garage, &self).await,
|
||||
AdminApiRequest::Metrics(req) => req.handle(&self.garage, &self).await,
|
||||
req => {
|
||||
let res = req.handle(&self.garage, &self).await?;
|
||||
let mut res = json_ok_response(&res)?;
|
||||
res.headers_mut()
|
||||
.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "metrics"))]
|
||||
Err(Error::bad_request(
|
||||
"Garage was built without the metrics feature".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ApiHandler for AdminApiServer {
|
||||
struct ArcAdminApiServer(Arc<AdminApiServer>);
|
||||
|
||||
impl ApiHandler for ArcAdminApiServer {
|
||||
const API_NAME: &'static str = "admin";
|
||||
const API_NAME_DISPLAY: &'static str = "Admin";
|
||||
|
||||
type Endpoint = Endpoint;
|
||||
type Endpoint = HttpEndpoint;
|
||||
type Error = Error;
|
||||
|
||||
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<Endpoint, Error> {
|
||||
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<HttpEndpoint, Error> {
|
||||
if req.uri().path().starts_with("/v0/") {
|
||||
let endpoint_v0 = router_v0::Endpoint::from_request(req)?;
|
||||
Endpoint::from_v0(endpoint_v0)
|
||||
let endpoint_v1 = router_v1::Endpoint::from_v0(endpoint_v0)?;
|
||||
Ok(HttpEndpoint::Old(endpoint_v1))
|
||||
} else if req.uri().path().starts_with("/v1/") {
|
||||
let endpoint_v1 = router_v1::Endpoint::from_request(req)?;
|
||||
Ok(HttpEndpoint::Old(endpoint_v1))
|
||||
} else {
|
||||
Endpoint::from_request(req)
|
||||
Ok(HttpEndpoint::New(req.uri().path().to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
req: Request<IncomingBody>,
|
||||
endpoint: Endpoint,
|
||||
endpoint: HttpEndpoint,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let required_auth_hash =
|
||||
match endpoint.authorization_type() {
|
||||
Authorization::None => None,
|
||||
Authorization::MetricsToken => self.metrics_token.as_deref(),
|
||||
Authorization::AdminToken => match self.admin_token.as_deref() {
|
||||
None => return Err(Error::forbidden(
|
||||
"Admin token isn't configured, admin API access is disabled for security.",
|
||||
)),
|
||||
Some(t) => Some(t),
|
||||
},
|
||||
};
|
||||
self.0.handle_http_api(req, endpoint).await
|
||||
}
|
||||
|
||||
if let Some(password_hash) = required_auth_hash {
|
||||
match req.headers().get("Authorization") {
|
||||
None => return Err(Error::forbidden("Authorization token must be provided")),
|
||||
Some(authorization) => {
|
||||
verify_bearer_token(&authorization, password_hash)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match endpoint {
|
||||
Endpoint::Options => self.handle_options(&req),
|
||||
Endpoint::CheckDomain => self.handle_check_domain(req).await,
|
||||
Endpoint::Health => self.handle_health(),
|
||||
Endpoint::Metrics => self.handle_metrics(),
|
||||
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,
|
||||
Endpoint::GetClusterHealth => handle_get_cluster_health(&self.garage).await,
|
||||
Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await,
|
||||
// Layout
|
||||
Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await,
|
||||
Endpoint::UpdateClusterLayout => handle_update_cluster_layout(&self.garage, req).await,
|
||||
Endpoint::ApplyClusterLayout => handle_apply_cluster_layout(&self.garage, req).await,
|
||||
Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage).await,
|
||||
// Keys
|
||||
Endpoint::ListKeys => handle_list_keys(&self.garage).await,
|
||||
Endpoint::GetKeyInfo {
|
||||
id,
|
||||
search,
|
||||
show_secret_key,
|
||||
} => {
|
||||
let show_secret_key = show_secret_key.map(|x| x == "true").unwrap_or(false);
|
||||
handle_get_key_info(&self.garage, id, search, show_secret_key).await
|
||||
}
|
||||
Endpoint::CreateKey => handle_create_key(&self.garage, req).await,
|
||||
Endpoint::ImportKey => handle_import_key(&self.garage, req).await,
|
||||
Endpoint::UpdateKey { id } => handle_update_key(&self.garage, id, req).await,
|
||||
Endpoint::DeleteKey { id } => handle_delete_key(&self.garage, id).await,
|
||||
// Buckets
|
||||
Endpoint::ListBuckets => handle_list_buckets(&self.garage).await,
|
||||
Endpoint::GetBucketInfo { id, global_alias } => {
|
||||
handle_get_bucket_info(&self.garage, id, global_alias).await
|
||||
}
|
||||
Endpoint::CreateBucket => handle_create_bucket(&self.garage, req).await,
|
||||
Endpoint::DeleteBucket { id } => handle_delete_bucket(&self.garage, id).await,
|
||||
Endpoint::UpdateBucket { id } => handle_update_bucket(&self.garage, id, req).await,
|
||||
// Bucket-key permissions
|
||||
Endpoint::BucketAllowKey => {
|
||||
handle_bucket_change_key_perm(&self.garage, req, true).await
|
||||
}
|
||||
Endpoint::BucketDenyKey => {
|
||||
handle_bucket_change_key_perm(&self.garage, req, false).await
|
||||
}
|
||||
// Bucket aliasing
|
||||
Endpoint::GlobalAliasBucket { id, alias } => {
|
||||
handle_global_alias_bucket(&self.garage, id, alias).await
|
||||
}
|
||||
Endpoint::GlobalUnaliasBucket { id, alias } => {
|
||||
handle_global_unalias_bucket(&self.garage, id, alias).await
|
||||
}
|
||||
Endpoint::LocalAliasBucket {
|
||||
id,
|
||||
access_key_id,
|
||||
alias,
|
||||
} => handle_local_alias_bucket(&self.garage, id, access_key_id, alias).await,
|
||||
Endpoint::LocalUnaliasBucket {
|
||||
id,
|
||||
access_key_id,
|
||||
alias,
|
||||
} => handle_local_unalias_bucket(&self.garage, id, access_key_id, alias).await,
|
||||
}
|
||||
fn key_id_from_request(&self, req: &Request<IncomingBody>) -> Option<String> {
|
||||
let auth_header = req.headers().get(AUTHORIZATION)?;
|
||||
let token = parse_authorization(auth_header).ok()?;
|
||||
let key_id = token.split_once('.')?.0;
|
||||
Some(key_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiEndpoint for Endpoint {
|
||||
fn name(&self) -> &'static str {
|
||||
Endpoint::name(self)
|
||||
impl ApiEndpoint for HttpEndpoint {
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
Self::Old(endpoint_v1) => Cow::Borrowed(endpoint_v1.name()),
|
||||
Self::New(path) => Cow::Owned(path.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_span_attributes(&self, _span: SpanRef<'_>) {}
|
||||
@@ -349,20 +251,91 @@ fn hash_bearer_token(token: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn verify_bearer_token(token: &hyper::http::HeaderValue, password_hash: &str) -> Result<(), Error> {
|
||||
use argon2::{password_hash::PasswordVerifier, Argon2};
|
||||
|
||||
let parsed_hash = PasswordHash::new(&password_hash).unwrap();
|
||||
|
||||
token
|
||||
fn parse_authorization(auth_header: &hyper::http::HeaderValue) -> Result<&str, Error> {
|
||||
let token = auth_header
|
||||
.to_str()?
|
||||
.strip_prefix("Bearer ")
|
||||
.and_then(|token| {
|
||||
Argon2::default()
|
||||
.verify_password(token.trim().as_bytes(), &parsed_hash)
|
||||
.ok()
|
||||
})
|
||||
.ok_or_else(|| Error::forbidden("Invalid authorization token"))?;
|
||||
.ok_or_else(|| Error::forbidden("Invalid Authorization header"))?
|
||||
.trim();
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
fn verify_authorization(
|
||||
garage: &Garage,
|
||||
global_token_hash: Option<&str>,
|
||||
auth_header: Option<hyper::http::HeaderValue>,
|
||||
endpoint_name: &str,
|
||||
) -> Result<(), Error> {
|
||||
use argon2::{password_hash::PasswordHash, password_hash::PasswordVerifier, Argon2};
|
||||
|
||||
let invalid_msg = "Invalid bearer token";
|
||||
|
||||
let token = match &auth_header {
|
||||
None => {
|
||||
return Err(Error::forbidden(
|
||||
"Bearer token must be provided in Authorization header",
|
||||
))
|
||||
}
|
||||
Some(authorization) => parse_authorization(authorization)?,
|
||||
};
|
||||
|
||||
let token_hash_string = if let Some((prefix, _)) = token.split_once('.') {
|
||||
garage
|
||||
.admin_token_table
|
||||
.get_local(&EmptyKey, &prefix.to_string())?
|
||||
.and_then(|k| k.state.into_option())
|
||||
.filter(|p| !p.is_expired(now_msec()))
|
||||
// GetCurrentAdminTokenInfo endpoint must be accessible even if it is not in the token scopes
|
||||
.filter(|p| p.has_scope(endpoint_name) || endpoint_name == "GetCurrentAdminTokenInfo")
|
||||
.ok_or_else(|| Error::forbidden(invalid_msg))?
|
||||
.token_hash
|
||||
} else {
|
||||
global_token_hash
|
||||
.ok_or_else(|| Error::forbidden(invalid_msg))?
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let token_hash =
|
||||
PasswordHash::new(&token_hash_string).ok_or_internal_error("Could not parse token hash")?;
|
||||
|
||||
Argon2::default()
|
||||
.verify_password(token.as_bytes(), &token_hash)
|
||||
.map_err(|_| Error::forbidden(invalid_msg))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn find_matching_nodes(garage: &Garage, spec: &str) -> Result<Vec<Uuid>, Error> {
|
||||
if spec == "self" {
|
||||
Ok(vec![garage.system.id])
|
||||
} else {
|
||||
// Collect all nodes currently up and/or in cluster layout
|
||||
let mut res = vec![];
|
||||
if let Ok(all_nodes) = garage.system.cluster_layout().all_nodes() {
|
||||
res = all_nodes.to_vec();
|
||||
}
|
||||
for node in garage.system.get_known_nodes() {
|
||||
if node.is_up && !res.contains(&node.id) {
|
||||
res.push(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
if spec == "*" {
|
||||
// match all nodes
|
||||
Ok(res)
|
||||
} else {
|
||||
// filter nodes that match spec
|
||||
res.retain(|node| hex::encode(node).starts_with(spec));
|
||||
if res.is_empty() {
|
||||
Err(Error::bad_request(format!("No nodes matching {}", spec)))
|
||||
} else if res.len() > 1 {
|
||||
Err(Error::bad_request(format!(
|
||||
"Multiple nodes matching {}: {:?}",
|
||||
spec, res
|
||||
)))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::Error as GarageError;
|
||||
use garage_util::time::now_msec;
|
||||
|
||||
use garage_table::EmptyKey;
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::s3::object_table::*;
|
||||
use garage_model::s3::version_table::*;
|
||||
|
||||
use garage_api_common::common_error::CommonErrorDerivative;
|
||||
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
use crate::{Admin, RequestHandler};
|
||||
|
||||
impl RequestHandler for LocalListBlockErrorsRequest {
|
||||
type Response = LocalListBlockErrorsResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<LocalListBlockErrorsResponse, Error> {
|
||||
let errors = garage.block_manager.list_resync_errors()?;
|
||||
let now = now_msec();
|
||||
let errors = errors
|
||||
.into_iter()
|
||||
.map(|e| BlockError {
|
||||
block_hash: hex::encode(&e.hash),
|
||||
refcount: e.refcount,
|
||||
error_count: e.error_count,
|
||||
last_try_secs_ago: now.saturating_sub(e.last_try) / 1000,
|
||||
next_try_in_secs: e.next_try.saturating_sub(now) / 1000,
|
||||
})
|
||||
.collect();
|
||||
Ok(LocalListBlockErrorsResponse(errors))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for LocalGetBlockInfoRequest {
|
||||
type Response = LocalGetBlockInfoResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<LocalGetBlockInfoResponse, Error> {
|
||||
let hash = find_block_hash_by_prefix(garage, &self.block_hash)?;
|
||||
let refcount = garage.block_manager.get_block_rc(&hash)?;
|
||||
let block_refs = garage
|
||||
.block_ref_table
|
||||
.get_range(&hash, None, None, 10000, Default::default())
|
||||
.await?;
|
||||
let mut versions = vec![];
|
||||
for br in block_refs {
|
||||
if let Some(v) = garage.version_table.get(&br.version, &EmptyKey).await? {
|
||||
let bl = match &v.backlink {
|
||||
VersionBacklink::MultipartUpload { upload_id } => {
|
||||
if let Some(u) = garage.mpu_table.get(upload_id, &EmptyKey).await? {
|
||||
BlockVersionBacklink::Upload {
|
||||
upload_id: hex::encode(&upload_id),
|
||||
upload_deleted: u.deleted.get(),
|
||||
upload_garbage_collected: false,
|
||||
bucket_id: Some(hex::encode(&u.bucket_id)),
|
||||
key: Some(u.key.to_string()),
|
||||
}
|
||||
} else {
|
||||
BlockVersionBacklink::Upload {
|
||||
upload_id: hex::encode(&upload_id),
|
||||
upload_deleted: true,
|
||||
upload_garbage_collected: true,
|
||||
bucket_id: None,
|
||||
key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
VersionBacklink::Object { bucket_id, key } => BlockVersionBacklink::Object {
|
||||
bucket_id: hex::encode(&bucket_id),
|
||||
key: key.to_string(),
|
||||
},
|
||||
};
|
||||
versions.push(BlockVersion {
|
||||
version_id: hex::encode(&br.version),
|
||||
ref_deleted: br.deleted.get(),
|
||||
version_deleted: v.deleted.get(),
|
||||
garbage_collected: false,
|
||||
backlink: Some(bl),
|
||||
});
|
||||
} else {
|
||||
versions.push(BlockVersion {
|
||||
version_id: hex::encode(&br.version),
|
||||
ref_deleted: br.deleted.get(),
|
||||
version_deleted: true,
|
||||
garbage_collected: true,
|
||||
backlink: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(LocalGetBlockInfoResponse {
|
||||
block_hash: hex::encode(&hash),
|
||||
refcount,
|
||||
versions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for LocalRetryBlockResyncRequest {
|
||||
type Response = LocalRetryBlockResyncResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<LocalRetryBlockResyncResponse, Error> {
|
||||
match self {
|
||||
Self::All { all: true } => {
|
||||
let blocks = garage.block_manager.list_resync_errors()?;
|
||||
for b in blocks.iter() {
|
||||
garage.block_manager.resync.clear_backoff(&b.hash)?;
|
||||
}
|
||||
Ok(LocalRetryBlockResyncResponse {
|
||||
count: blocks.len() as u64,
|
||||
})
|
||||
}
|
||||
Self::All { all: false } => Err(Error::bad_request("nonsense")),
|
||||
Self::Blocks { block_hashes } => {
|
||||
for hash in block_hashes.iter() {
|
||||
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
|
||||
let hash = Hash::try_from(&hash).ok_or_bad_request("invalid hash")?;
|
||||
garage.block_manager.resync.clear_backoff(&hash)?;
|
||||
}
|
||||
Ok(LocalRetryBlockResyncResponse {
|
||||
count: block_hashes.len() as u64,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for LocalPurgeBlocksRequest {
|
||||
type Response = LocalPurgeBlocksResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<LocalPurgeBlocksResponse, Error> {
|
||||
let mut obj_dels = 0;
|
||||
let mut mpu_dels = 0;
|
||||
let mut ver_dels = 0;
|
||||
let mut br_dels = 0;
|
||||
|
||||
for hash in self.0.iter() {
|
||||
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
|
||||
let hash = Hash::try_from(&hash).ok_or_bad_request("invalid hash")?;
|
||||
let block_refs = garage
|
||||
.block_ref_table
|
||||
.get_range(&hash, None, None, 10000, Default::default())
|
||||
.await?;
|
||||
|
||||
for br in block_refs {
|
||||
if let Some(version) = garage.version_table.get(&br.version, &EmptyKey).await? {
|
||||
handle_block_purge_version_backlink(
|
||||
garage,
|
||||
&version,
|
||||
&mut obj_dels,
|
||||
&mut mpu_dels,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !version.deleted.get() {
|
||||
let deleted_version = Version::new(version.uuid, version.backlink, true);
|
||||
garage.version_table.insert(&deleted_version).await?;
|
||||
ver_dels += 1;
|
||||
}
|
||||
}
|
||||
if !br.deleted.get() {
|
||||
let mut br = br;
|
||||
br.deleted.set();
|
||||
garage.block_ref_table.insert(&br).await?;
|
||||
br_dels += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LocalPurgeBlocksResponse {
|
||||
blocks_purged: self.0.len() as u64,
|
||||
block_refs_purged: br_dels,
|
||||
versions_deleted: ver_dels,
|
||||
objects_deleted: obj_dels,
|
||||
uploads_deleted: mpu_dels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn find_block_hash_by_prefix(garage: &Arc<Garage>, prefix: &str) -> Result<Hash, Error> {
|
||||
if prefix.len() < 4 {
|
||||
return Err(Error::bad_request(
|
||||
"Please specify at least 4 characters of the block hash",
|
||||
));
|
||||
}
|
||||
|
||||
let prefix_bin = hex::decode(&prefix[..prefix.len() & !1]).ok_or_bad_request("invalid hash")?;
|
||||
|
||||
let iter = garage
|
||||
.block_ref_table
|
||||
.data
|
||||
.store
|
||||
.range(&prefix_bin[..]..)
|
||||
.map_err(GarageError::from)?;
|
||||
let mut found = None;
|
||||
for item in iter {
|
||||
let (k, _v) = item.map_err(GarageError::from)?;
|
||||
let hash = Hash::try_from(&k[..32]).unwrap();
|
||||
if &hash.as_slice()[..prefix_bin.len()] != prefix_bin {
|
||||
break;
|
||||
}
|
||||
if hex::encode(hash.as_slice()).starts_with(prefix) {
|
||||
match &found {
|
||||
Some(x) if *x == hash => (),
|
||||
Some(_) => {
|
||||
return Err(Error::bad_request(format!(
|
||||
"Several blocks match prefix `{}`",
|
||||
prefix
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
found = Some(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
found.ok_or_else(|| Error::NoSuchBlock(prefix.to_string()))
|
||||
}
|
||||
|
||||
async fn handle_block_purge_version_backlink(
|
||||
garage: &Arc<Garage>,
|
||||
version: &Version,
|
||||
obj_dels: &mut u64,
|
||||
mpu_dels: &mut u64,
|
||||
) -> Result<(), Error> {
|
||||
let (bucket_id, key, ov_id) = match &version.backlink {
|
||||
VersionBacklink::Object { bucket_id, key } => (*bucket_id, key.clone(), version.uuid),
|
||||
VersionBacklink::MultipartUpload { upload_id } => {
|
||||
if let Some(mut mpu) = garage.mpu_table.get(upload_id, &EmptyKey).await? {
|
||||
if !mpu.deleted.get() {
|
||||
mpu.parts.clear();
|
||||
mpu.deleted.set();
|
||||
garage.mpu_table.insert(&mpu).await?;
|
||||
*mpu_dels += 1;
|
||||
}
|
||||
(mpu.bucket_id, mpu.key.clone(), *upload_id)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(object) = garage.object_table.get(&bucket_id, &key).await? {
|
||||
let ov = object.versions().iter().rev().find(|v| v.is_complete());
|
||||
if let Some(ov) = ov {
|
||||
if ov.uuid == ov_id {
|
||||
let del_uuid = gen_uuid();
|
||||
let deleted_object = Object::new(
|
||||
bucket_id,
|
||||
key,
|
||||
vec![ObjectVersion {
|
||||
uuid: del_uuid,
|
||||
timestamp: ov.timestamp + 1,
|
||||
state: ObjectVersionState::Complete(ObjectVersionData::DeleteMarker),
|
||||
}],
|
||||
);
|
||||
garage.object_table.insert(&deleted_object).await?;
|
||||
*obj_dels += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,410 +1,288 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use hyper::{body::Incoming as IncomingBody, Request, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use format_table::format_table_to_string;
|
||||
|
||||
use garage_util::crdt::*;
|
||||
use garage_util::data::*;
|
||||
|
||||
use garage_rpc::layout;
|
||||
use garage_rpc::layout::PARTITION_BITS;
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
|
||||
use crate::admin::api_server::ResBody;
|
||||
use crate::admin::error::*;
|
||||
use crate::helpers::{json_ok_response, parse_json_body};
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
use crate::{Admin, RequestHandler};
|
||||
|
||||
pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
|
||||
let layout = garage.system.cluster_layout();
|
||||
let mut nodes = garage
|
||||
.system
|
||||
.get_known_nodes()
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
(
|
||||
i.id,
|
||||
NodeResp {
|
||||
id: hex::encode(i.id),
|
||||
addr: i.addr,
|
||||
hostname: i.status.hostname,
|
||||
is_up: i.is_up,
|
||||
last_seen_secs_ago: i.last_seen_secs_ago,
|
||||
data_partition: i
|
||||
.status
|
||||
.data_disk_avail
|
||||
.map(|(avail, total)| FreeSpaceResp {
|
||||
available: avail,
|
||||
total,
|
||||
impl RequestHandler for GetClusterStatusRequest {
|
||||
type Response = GetClusterStatusResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<GetClusterStatusResponse, Error> {
|
||||
let layout = garage.system.cluster_layout();
|
||||
let mut nodes = garage
|
||||
.system
|
||||
.get_known_nodes()
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
(
|
||||
i.id,
|
||||
NodeResp {
|
||||
id: hex::encode(i.id),
|
||||
garage_version: i.status.garage_version,
|
||||
addr: i.addr,
|
||||
hostname: i.status.hostname,
|
||||
is_up: i.is_up,
|
||||
last_seen_secs_ago: i.last_seen_secs_ago,
|
||||
data_partition: i.status.data_disk_avail.map(|(avail, total)| {
|
||||
FreeSpaceResp {
|
||||
available: avail,
|
||||
total,
|
||||
}
|
||||
}),
|
||||
metadata_partition: i.status.meta_disk_avail.map(|(avail, total)| {
|
||||
FreeSpaceResp {
|
||||
available: avail,
|
||||
total,
|
||||
}
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
metadata_partition: i.status.meta_disk_avail.map(|(avail, total)| {
|
||||
FreeSpaceResp {
|
||||
available: avail,
|
||||
total,
|
||||
}
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
for (id, _, role) in layout.current().roles.items().iter() {
|
||||
if let layout::NodeRoleV(Some(r)) = role {
|
||||
let role = NodeRoleResp {
|
||||
id: hex::encode(id),
|
||||
zone: r.zone.to_string(),
|
||||
capacity: r.capacity,
|
||||
tags: r.tags.clone(),
|
||||
};
|
||||
match nodes.get_mut(id) {
|
||||
None => {
|
||||
nodes.insert(
|
||||
*id,
|
||||
NodeResp {
|
||||
id: hex::encode(id),
|
||||
role: Some(role),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(n) => {
|
||||
n.role = Some(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ver in layout.versions().iter().rev().skip(1) {
|
||||
for (id, _, role) in ver.roles.items().iter() {
|
||||
if let layout::NodeRoleV(Some(r)) = role {
|
||||
if r.capacity.is_some() {
|
||||
if let Some(n) = nodes.get_mut(id) {
|
||||
if n.role.is_none() {
|
||||
n.draining = true;
|
||||
if let Ok(current_layout) = layout.current() {
|
||||
for (id, _, role) in current_layout.roles.items().iter() {
|
||||
if let layout::NodeRoleV(Some(r)) = role {
|
||||
let role = NodeAssignedRole {
|
||||
zone: r.zone.to_string(),
|
||||
capacity: r.capacity,
|
||||
tags: r.tags.clone(),
|
||||
};
|
||||
match nodes.get_mut(id) {
|
||||
None => {
|
||||
nodes.insert(
|
||||
*id,
|
||||
NodeResp {
|
||||
id: hex::encode(id),
|
||||
role: Some(role),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(n) => {
|
||||
n.role = Some(role);
|
||||
}
|
||||
} else {
|
||||
nodes.insert(
|
||||
*id,
|
||||
NodeResp {
|
||||
id: hex::encode(id),
|
||||
draining: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut nodes = nodes.into_values().collect::<Vec<_>>();
|
||||
nodes.sort_by(|x, y| x.id.cmp(&y.id));
|
||||
if let Ok(layout_versions) = layout.versions() {
|
||||
for ver in layout_versions.iter().rev().skip(1) {
|
||||
for (id, _, role) in ver.roles.items().iter() {
|
||||
if let layout::NodeRoleV(Some(r)) = role {
|
||||
if r.capacity.is_some() {
|
||||
if let Some(n) = nodes.get_mut(id) {
|
||||
if n.role.is_none() {
|
||||
n.draining = true;
|
||||
}
|
||||
} else {
|
||||
nodes.insert(
|
||||
*id,
|
||||
NodeResp {
|
||||
id: hex::encode(id),
|
||||
draining: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let res = GetClusterStatusResponse {
|
||||
node: hex::encode(garage.system.id),
|
||||
garage_version: garage_util::version::garage_version(),
|
||||
garage_features: garage_util::version::garage_features(),
|
||||
rust_version: garage_util::version::rust_version(),
|
||||
db_engine: garage.db.engine(),
|
||||
layout_version: layout.current().version,
|
||||
nodes,
|
||||
};
|
||||
let mut nodes = nodes.into_values().collect::<Vec<_>>();
|
||||
nodes.sort_by(|x, y| x.id.cmp(&y.id));
|
||||
|
||||
Ok(json_ok_response(&res)?)
|
||||
}
|
||||
|
||||
pub async fn handle_get_cluster_health(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
|
||||
use garage_rpc::system::ClusterHealthStatus;
|
||||
let health = garage.system.health();
|
||||
let health = ClusterHealth {
|
||||
status: match health.status {
|
||||
ClusterHealthStatus::Healthy => "healthy",
|
||||
ClusterHealthStatus::Degraded => "degraded",
|
||||
ClusterHealthStatus::Unavailable => "unavailable",
|
||||
},
|
||||
known_nodes: health.known_nodes,
|
||||
connected_nodes: health.connected_nodes,
|
||||
storage_nodes: health.storage_nodes,
|
||||
storage_nodes_ok: health.storage_nodes_ok,
|
||||
partitions: health.partitions,
|
||||
partitions_quorum: health.partitions_quorum,
|
||||
partitions_all_ok: health.partitions_all_ok,
|
||||
};
|
||||
Ok(json_ok_response(&health)?)
|
||||
}
|
||||
|
||||
pub async fn handle_connect_cluster_nodes(
|
||||
garage: &Arc<Garage>,
|
||||
req: Request<IncomingBody>,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let req = parse_json_body::<Vec<String>, _, Error>(req).await?;
|
||||
|
||||
let res = futures::future::join_all(req.iter().map(|node| garage.system.connect(node)))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|r| match r {
|
||||
Ok(()) => ConnectClusterNodesResponse {
|
||||
success: true,
|
||||
error: None,
|
||||
},
|
||||
Err(e) => ConnectClusterNodesResponse {
|
||||
success: false,
|
||||
error: Some(format!("{}", e)),
|
||||
},
|
||||
Ok(GetClusterStatusResponse {
|
||||
layout_version: layout.inner().current().version,
|
||||
nodes,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(json_ok_response(&res)?)
|
||||
}
|
||||
|
||||
pub async fn handle_get_cluster_layout(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
|
||||
let res = format_cluster_layout(garage.system.cluster_layout().inner());
|
||||
|
||||
Ok(json_ok_response(&res)?)
|
||||
}
|
||||
|
||||
fn format_cluster_layout(layout: &layout::LayoutHistory) -> GetClusterLayoutResponse {
|
||||
let roles = layout
|
||||
.current()
|
||||
.roles
|
||||
.items()
|
||||
.iter()
|
||||
.filter_map(|(k, _, v)| v.0.clone().map(|x| (k, x)))
|
||||
.map(|(k, v)| NodeRoleResp {
|
||||
id: hex::encode(k),
|
||||
zone: v.zone.clone(),
|
||||
capacity: v.capacity,
|
||||
tags: v.tags.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let staged_role_changes = layout
|
||||
.staging
|
||||
.get()
|
||||
.roles
|
||||
.items()
|
||||
.iter()
|
||||
.filter(|(k, _, v)| layout.current().roles.get(k) != Some(v))
|
||||
.map(|(k, _, v)| match &v.0 {
|
||||
None => NodeRoleChange {
|
||||
id: hex::encode(k),
|
||||
action: NodeRoleChangeEnum::Remove { remove: true },
|
||||
},
|
||||
Some(r) => NodeRoleChange {
|
||||
id: hex::encode(k),
|
||||
action: NodeRoleChangeEnum::Update {
|
||||
zone: r.zone.clone(),
|
||||
capacity: r.capacity,
|
||||
tags: r.tags.clone(),
|
||||
},
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
GetClusterLayoutResponse {
|
||||
version: layout.current().version,
|
||||
roles,
|
||||
staged_role_changes,
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
impl RequestHandler for GetClusterHealthRequest {
|
||||
type Response = GetClusterHealthResponse;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClusterHealth {
|
||||
status: &'static str,
|
||||
known_nodes: usize,
|
||||
connected_nodes: usize,
|
||||
storage_nodes: usize,
|
||||
storage_nodes_ok: usize,
|
||||
partitions: usize,
|
||||
partitions_quorum: usize,
|
||||
partitions_all_ok: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetClusterStatusResponse {
|
||||
node: String,
|
||||
garage_version: &'static str,
|
||||
garage_features: Option<&'static [&'static str]>,
|
||||
rust_version: &'static str,
|
||||
db_engine: String,
|
||||
layout_version: u64,
|
||||
nodes: Vec<NodeResp>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ApplyClusterLayoutResponse {
|
||||
message: Vec<String>,
|
||||
layout: GetClusterLayoutResponse,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConnectClusterNodesResponse {
|
||||
success: bool,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetClusterLayoutResponse {
|
||||
version: u64,
|
||||
roles: Vec<NodeRoleResp>,
|
||||
staged_role_changes: Vec<NodeRoleChange>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NodeRoleResp {
|
||||
id: String,
|
||||
zone: String,
|
||||
capacity: Option<u64>,
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct FreeSpaceResp {
|
||||
available: u64,
|
||||
total: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NodeResp {
|
||||
id: String,
|
||||
role: Option<NodeRoleResp>,
|
||||
addr: Option<SocketAddr>,
|
||||
hostname: Option<String>,
|
||||
is_up: bool,
|
||||
last_seen_secs_ago: Option<u64>,
|
||||
draining: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
data_partition: Option<FreeSpaceResp>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
metadata_partition: Option<FreeSpaceResp>,
|
||||
}
|
||||
|
||||
// ---- update functions ----
|
||||
|
||||
pub async fn handle_update_cluster_layout(
|
||||
garage: &Arc<Garage>,
|
||||
req: Request<IncomingBody>,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let updates = parse_json_body::<UpdateClusterLayoutRequest, _, Error>(req).await?;
|
||||
|
||||
let mut layout = garage.system.cluster_layout().inner().clone();
|
||||
|
||||
let mut roles = layout.current().roles.clone();
|
||||
roles.merge(&layout.staging.get().roles);
|
||||
|
||||
for change in updates {
|
||||
let node = hex::decode(&change.id).ok_or_bad_request("Invalid node identifier")?;
|
||||
let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?;
|
||||
|
||||
let new_role = match change.action {
|
||||
NodeRoleChangeEnum::Remove { remove: true } => None,
|
||||
NodeRoleChangeEnum::Update {
|
||||
zone,
|
||||
capacity,
|
||||
tags,
|
||||
} => Some(layout::NodeRole {
|
||||
zone,
|
||||
capacity,
|
||||
tags,
|
||||
}),
|
||||
_ => return Err(Error::bad_request("Invalid layout change")),
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<GetClusterHealthResponse, Error> {
|
||||
use garage_rpc::system::ClusterHealthStatus;
|
||||
let health = garage.system.health();
|
||||
let health = GetClusterHealthResponse {
|
||||
status: match health.status {
|
||||
ClusterHealthStatus::Healthy => "healthy",
|
||||
ClusterHealthStatus::Degraded => "degraded",
|
||||
ClusterHealthStatus::Unavailable => "unavailable",
|
||||
}
|
||||
.to_string(),
|
||||
known_nodes: health.known_nodes,
|
||||
connected_nodes: health.connected_nodes,
|
||||
storage_nodes: health.storage_nodes,
|
||||
// Translating storage_nodes_up (admin API context) to storage_nodes_ok (metrics context)
|
||||
// TODO: when releasing major release, consider renaming all the fields in the metrics to storage_nodes_up
|
||||
storage_nodes_up: health.storage_nodes_ok,
|
||||
partitions: health.partitions,
|
||||
partitions_quorum: health.partitions_quorum,
|
||||
partitions_all_ok: health.partitions_all_ok,
|
||||
};
|
||||
|
||||
layout
|
||||
.staging
|
||||
.get_mut()
|
||||
.roles
|
||||
.merge(&roles.update_mutator(node, layout::NodeRoleV(new_role)));
|
||||
Ok(health)
|
||||
}
|
||||
|
||||
garage
|
||||
.system
|
||||
.layout_manager
|
||||
.update_cluster_layout(&layout)
|
||||
.await?;
|
||||
|
||||
let res = format_cluster_layout(&layout);
|
||||
Ok(json_ok_response(&res)?)
|
||||
}
|
||||
|
||||
pub async fn handle_apply_cluster_layout(
|
||||
garage: &Arc<Garage>,
|
||||
req: Request<IncomingBody>,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let param = parse_json_body::<ApplyLayoutRequest, _, Error>(req).await?;
|
||||
impl RequestHandler for GetClusterStatisticsRequest {
|
||||
type Response = GetClusterStatisticsResponse;
|
||||
|
||||
let layout = garage.system.cluster_layout().inner().clone();
|
||||
let (layout, msg) = layout.apply_staged_changes(Some(param.version))?;
|
||||
// FIXME: return this as a JSON struct instead of text
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<GetClusterStatisticsResponse, Error> {
|
||||
let mut ret = String::new();
|
||||
|
||||
garage
|
||||
.system
|
||||
.layout_manager
|
||||
.update_cluster_layout(&layout)
|
||||
.await?;
|
||||
// Gather storage node and free space statistics for current nodes
|
||||
let layout = &garage.system.cluster_layout();
|
||||
let mut node_partition_count = HashMap::<Uuid, u64>::new();
|
||||
if let Ok(current_layout) = layout.current() {
|
||||
for short_id in current_layout.ring_assignment_data.iter() {
|
||||
let id = current_layout.node_id_vec[*short_id as usize];
|
||||
*node_partition_count.entry(id).or_default() += 1;
|
||||
}
|
||||
}
|
||||
let node_info = garage
|
||||
.system
|
||||
.get_known_nodes()
|
||||
.into_iter()
|
||||
.map(|n| (n.id, n))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let res = ApplyClusterLayoutResponse {
|
||||
message: msg,
|
||||
layout: format_cluster_layout(&layout),
|
||||
};
|
||||
Ok(json_ok_response(&res)?)
|
||||
let mut table = vec![" ID\tHostname\tZone\tCapacity\tPart.\tDataAvail\tMetaAvail".into()];
|
||||
for (id, parts) in node_partition_count.iter() {
|
||||
let info = node_info.get(id);
|
||||
let status = info.map(|x| &x.status);
|
||||
let role = layout
|
||||
.current()
|
||||
.ok()
|
||||
.and_then(|l| l.roles.get(id))
|
||||
.and_then(|x| x.0.as_ref());
|
||||
let hostname = status.and_then(|x| x.hostname.as_deref()).unwrap_or("?");
|
||||
let zone = role.map(|x| x.zone.as_str()).unwrap_or("?");
|
||||
let capacity = role
|
||||
.map(|x| x.capacity_string())
|
||||
.unwrap_or_else(|| "?".into());
|
||||
let avail_str = |x| match x {
|
||||
Some((avail, total)) => {
|
||||
let pct = (avail as f64) / (total as f64) * 100.;
|
||||
let avail = bytesize::ByteSize::b(avail);
|
||||
let total = bytesize::ByteSize::b(total);
|
||||
format!("{}/{} ({:.1}%)", avail, total, pct)
|
||||
}
|
||||
None => "?".into(),
|
||||
};
|
||||
let data_avail = avail_str(status.and_then(|x| x.data_disk_avail));
|
||||
let meta_avail = avail_str(status.and_then(|x| x.meta_disk_avail));
|
||||
table.push(format!(
|
||||
" {:?}\t{}\t{}\t{}\t{}\t{}\t{}",
|
||||
id, hostname, zone, capacity, parts, data_avail, meta_avail
|
||||
));
|
||||
}
|
||||
write!(
|
||||
&mut ret,
|
||||
"Storage nodes:\n{}",
|
||||
format_table_to_string(table)
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let meta_part_avail = node_partition_count
|
||||
.iter()
|
||||
.filter_map(|(id, parts)| {
|
||||
node_info
|
||||
.get(id)
|
||||
.and_then(|x| x.status.meta_disk_avail)
|
||||
.map(|c| c.0 / *parts)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let data_part_avail = node_partition_count
|
||||
.iter()
|
||||
.filter_map(|(id, parts)| {
|
||||
node_info
|
||||
.get(id)
|
||||
.and_then(|x| x.status.data_disk_avail)
|
||||
.map(|c| c.0 / *parts)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !meta_part_avail.is_empty() && !data_part_avail.is_empty() {
|
||||
let meta_avail =
|
||||
bytesize::ByteSize(meta_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS));
|
||||
let data_avail =
|
||||
bytesize::ByteSize(data_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS));
|
||||
writeln!(
|
||||
&mut ret,
|
||||
"\nEstimated available storage space cluster-wide (might be lower in practice):"
|
||||
)
|
||||
.unwrap();
|
||||
if meta_part_avail.len() < node_partition_count.len()
|
||||
|| data_part_avail.len() < node_partition_count.len()
|
||||
{
|
||||
ret += &format_table_to_string(vec![
|
||||
format!(" data: < {}", data_avail),
|
||||
format!(" metadata: < {}", meta_avail),
|
||||
]);
|
||||
writeln!(&mut ret, "A precise estimate could not be given as information is missing for some storage nodes.").unwrap();
|
||||
} else {
|
||||
ret += &format_table_to_string(vec![
|
||||
format!(" data: {}", data_avail),
|
||||
format!(" metadata: {}", meta_avail),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GetClusterStatisticsResponse { freeform: ret })
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_revert_cluster_layout(
|
||||
garage: &Arc<Garage>,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let layout = garage.system.cluster_layout().inner().clone();
|
||||
let layout = layout.revert_staged_changes()?;
|
||||
garage
|
||||
.system
|
||||
.layout_manager
|
||||
.update_cluster_layout(&layout)
|
||||
.await?;
|
||||
impl RequestHandler for ConnectClusterNodesRequest {
|
||||
type Response = ConnectClusterNodesResponse;
|
||||
|
||||
let res = format_cluster_layout(&layout);
|
||||
Ok(json_ok_response(&res)?)
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
type UpdateClusterLayoutRequest = Vec<NodeRoleChange>;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ApplyLayoutRequest {
|
||||
version: u64,
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NodeRoleChange {
|
||||
id: String,
|
||||
#[serde(flatten)]
|
||||
action: NodeRoleChangeEnum,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum NodeRoleChangeEnum {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Remove { remove: bool },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Update {
|
||||
zone: String,
|
||||
capacity: Option<u64>,
|
||||
tags: Vec<String>,
|
||||
},
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<ConnectClusterNodesResponse, Error> {
|
||||
let res = futures::future::join_all(self.0.iter().map(|node| garage.system.connect(node)))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|r| match r {
|
||||
Ok(()) => ConnectNodeResponse {
|
||||
success: true,
|
||||
error: None,
|
||||
},
|
||||
Err(e) => ConnectNodeResponse {
|
||||
success: false,
|
||||
error: Some(format!("{}", e)),
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(ConnectClusterNodesResponse(res))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,76 @@
|
||||
use err_derive::Error;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use hyper::header::HeaderValue;
|
||||
use hyper::{HeaderMap, StatusCode};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use garage_model::helper::error::Error as HelperError;
|
||||
|
||||
use crate::common_error::CommonError;
|
||||
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
|
||||
use crate::generic_server::ApiError;
|
||||
use crate::helpers::*;
|
||||
use garage_api_common::common_error::{commonErrorDerivative, CommonError};
|
||||
pub use garage_api_common::common_error::{
|
||||
CommonErrorDerivative, OkOrBadRequest, OkOrInternalError,
|
||||
};
|
||||
use garage_api_common::generic_server::ApiError;
|
||||
use garage_api_common::helpers::*;
|
||||
|
||||
/// Errors of this crate
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(display = "{}", _0)]
|
||||
#[error("{0}")]
|
||||
/// Error from common error
|
||||
Common(CommonError),
|
||||
Common(#[from] CommonError),
|
||||
|
||||
// Category: cannot process
|
||||
/// The admin API token does not exist
|
||||
#[error("Admin token not found: {0}")]
|
||||
NoSuchAdminToken(String),
|
||||
|
||||
/// The API access key does not exist
|
||||
#[error(display = "Access key not found: {}", _0)]
|
||||
#[error("Access key not found: {0}")]
|
||||
NoSuchAccessKey(String),
|
||||
|
||||
/// The requested block does not exist
|
||||
#[error("Block not found: {0}")]
|
||||
NoSuchBlock(String),
|
||||
|
||||
/// The requested worker does not exist
|
||||
#[error("Worker not found: {0}")]
|
||||
NoSuchWorker(u64),
|
||||
|
||||
/// The object requested don't exists
|
||||
#[error("Key not found")]
|
||||
NoSuchKey,
|
||||
|
||||
/// In Import key, the key already exists
|
||||
#[error(
|
||||
display = "Key {} already exists in data store. Even if it is deleted, we can't let you create a new key with the same ID. Sorry.",
|
||||
_0
|
||||
)]
|
||||
#[error("Key {0} already exists in data store. Even if it is deleted, we can't let you create a new key with the same ID. Sorry.")]
|
||||
KeyAlreadyExists(String),
|
||||
}
|
||||
|
||||
impl<T> From<T> for Error
|
||||
where
|
||||
CommonError: From<T>,
|
||||
{
|
||||
fn from(err: T) -> Self {
|
||||
Error::Common(CommonError::from(err))
|
||||
commonErrorDerivative!(Error);
|
||||
|
||||
/// FIXME: helper errors are transformed into their corresponding variants
|
||||
/// in the Error struct, but in many case a helper error should be considered
|
||||
/// an internal error.
|
||||
impl From<HelperError> for Error {
|
||||
fn from(err: HelperError) -> Error {
|
||||
match CommonError::try_from(err) {
|
||||
Ok(ce) => Self::Common(ce),
|
||||
Err(HelperError::NoSuchAccessKey(k)) => Self::NoSuchAccessKey(k),
|
||||
Err(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommonErrorDerivative for Error {}
|
||||
|
||||
impl Error {
|
||||
fn code(&self) -> &'static str {
|
||||
pub fn code(&self) -> &'static str {
|
||||
match self {
|
||||
Error::Common(c) => c.aws_code(),
|
||||
Error::NoSuchAdminToken(_) => "NoSuchAdminToken",
|
||||
Error::NoSuchAccessKey(_) => "NoSuchAccessKey",
|
||||
Error::NoSuchWorker(_) => "NoSuchWorker",
|
||||
Error::NoSuchBlock(_) => "NoSuchBlock",
|
||||
Error::KeyAlreadyExists(_) => "KeyAlreadyExists",
|
||||
Error::NoSuchKey => "NoSuchKey",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +80,11 @@ impl ApiError for Error {
|
||||
fn http_status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Error::Common(c) => c.http_status_code(),
|
||||
Error::NoSuchAccessKey(_) => StatusCode::NOT_FOUND,
|
||||
Error::NoSuchAdminToken(_)
|
||||
| Error::NoSuchAccessKey(_)
|
||||
| Error::NoSuchWorker(_)
|
||||
| Error::NoSuchBlock(_)
|
||||
| Error::NoSuchKey => StatusCode::NOT_FOUND,
|
||||
Error::KeyAlreadyExists(_) => StatusCode::CONFLICT,
|
||||
}
|
||||
}
|
||||
@@ -63,6 +92,7 @@ impl ApiError for Error {
|
||||
fn add_http_headers(&self, header_map: &mut HeaderMap<HeaderValue>) {
|
||||
use hyper::header;
|
||||
header_map.append(header::CONTENT_TYPE, "application/json".parse().unwrap());
|
||||
header_map.append(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap());
|
||||
}
|
||||
|
||||
fn http_body(&self, garage_region: &str, path: &str) -> ErrorBody {
|
||||
|
||||
@@ -1,172 +1,190 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::DateTime;
|
||||
|
||||
use garage_table::*;
|
||||
use garage_util::time::now_msec;
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::key_table::*;
|
||||
|
||||
use crate::admin::api_server::ResBody;
|
||||
use crate::admin::error::*;
|
||||
use crate::helpers::*;
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
use crate::{Admin, RequestHandler};
|
||||
|
||||
pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
|
||||
let res = garage
|
||||
.key_table
|
||||
.get_range(
|
||||
&EmptyKey,
|
||||
None,
|
||||
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
|
||||
10000,
|
||||
EnumerationOrder::Forward,
|
||||
)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|k| ListKeyResultItem {
|
||||
id: k.key_id.to_string(),
|
||||
name: k.params().unwrap().name.get().clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
impl RequestHandler for ListKeysRequest {
|
||||
type Response = ListKeysResponse;
|
||||
|
||||
Ok(json_ok_response(&res)?)
|
||||
}
|
||||
async fn handle(self, garage: &Arc<Garage>, _admin: &Admin) -> Result<ListKeysResponse, Error> {
|
||||
let now = now_msec();
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ListKeyResultItem {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub async fn handle_get_key_info(
|
||||
garage: &Arc<Garage>,
|
||||
id: Option<String>,
|
||||
search: Option<String>,
|
||||
show_secret_key: bool,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let key = if let Some(id) = id {
|
||||
garage.key_helper().get_existing_key(&id).await?
|
||||
} else if let Some(search) = search {
|
||||
garage
|
||||
.key_helper()
|
||||
.get_existing_matching_key(&search)
|
||||
let res = garage
|
||||
.key_table
|
||||
.get_range(
|
||||
&EmptyKey,
|
||||
None,
|
||||
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
|
||||
10000,
|
||||
EnumerationOrder::Forward,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
.iter()
|
||||
.map(|k| {
|
||||
let p = k.params().unwrap();
|
||||
|
||||
key_info_results(garage, key, show_secret_key).await
|
||||
}
|
||||
ListKeysResponseItem {
|
||||
id: k.key_id.to_string(),
|
||||
name: p.name.get().clone(),
|
||||
created: p.created.map(|x| {
|
||||
DateTime::from_timestamp_millis(x as i64)
|
||||
.expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expiration: p.expiration.get().map(|x| {
|
||||
DateTime::from_timestamp_millis(x as i64)
|
||||
.expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expired: p.is_expired(now),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
pub async fn handle_create_key(
|
||||
garage: &Arc<Garage>,
|
||||
req: Request<IncomingBody>,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let req = parse_json_body::<CreateKeyRequest, _, Error>(req).await?;
|
||||
|
||||
let key = Key::new(req.name.as_deref().unwrap_or("Unnamed key"));
|
||||
garage.key_table.insert(&key).await?;
|
||||
|
||||
key_info_results(garage, key, true).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CreateKeyRequest {
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn handle_import_key(
|
||||
garage: &Arc<Garage>,
|
||||
req: Request<IncomingBody>,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let req = parse_json_body::<ImportKeyRequest, _, Error>(req).await?;
|
||||
|
||||
let prev_key = garage.key_table.get(&EmptyKey, &req.access_key_id).await?;
|
||||
if prev_key.is_some() {
|
||||
return Err(Error::KeyAlreadyExists(req.access_key_id.to_string()));
|
||||
Ok(ListKeysResponse(res))
|
||||
}
|
||||
|
||||
let imported_key = Key::import(
|
||||
&req.access_key_id,
|
||||
&req.secret_access_key,
|
||||
req.name.as_deref().unwrap_or("Imported key"),
|
||||
)
|
||||
.ok_or_bad_request("Invalid key format")?;
|
||||
garage.key_table.insert(&imported_key).await?;
|
||||
|
||||
key_info_results(garage, imported_key, false).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ImportKeyRequest {
|
||||
access_key_id: String,
|
||||
secret_access_key: String,
|
||||
name: Option<String>,
|
||||
}
|
||||
impl RequestHandler for GetKeyInfoRequest {
|
||||
type Response = GetKeyInfoResponse;
|
||||
|
||||
pub async fn handle_update_key(
|
||||
garage: &Arc<Garage>,
|
||||
id: String,
|
||||
req: Request<IncomingBody>,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let req = parse_json_body::<UpdateKeyRequest, _, Error>(req).await?;
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<GetKeyInfoResponse, Error> {
|
||||
let key = match (self.id, self.search) {
|
||||
(Some(id), None) => garage.key_helper().get_existing_key(&id).await?,
|
||||
(None, Some(search)) => {
|
||||
let candidates = garage
|
||||
.key_table
|
||||
.get_range(
|
||||
&EmptyKey,
|
||||
None,
|
||||
Some(KeyFilter::MatchesAndNotDeleted(search.to_string())),
|
||||
10,
|
||||
EnumerationOrder::Forward,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
if candidates.len() != 1 {
|
||||
return Err(Error::bad_request(format!(
|
||||
"{} matching keys",
|
||||
candidates.len()
|
||||
)));
|
||||
}
|
||||
candidates.into_iter().next().unwrap()
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::bad_request(
|
||||
"Either id or search must be provided (but not both)",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mut key = garage.key_helper().get_existing_key(&id).await?;
|
||||
|
||||
let key_state = key.state.as_option_mut().unwrap();
|
||||
|
||||
if let Some(new_name) = req.name {
|
||||
key_state.name.update(new_name);
|
||||
Ok(key_info_results(garage, key, self.show_secret_key).await?)
|
||||
}
|
||||
if let Some(allow) = req.allow {
|
||||
if allow.create_bucket {
|
||||
key_state.allow_create_bucket.update(true);
|
||||
}
|
||||
|
||||
impl RequestHandler for CreateKeyRequest {
|
||||
type Response = CreateKeyResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<CreateKeyResponse, Error> {
|
||||
let mut key = Key::new("Unnamed key");
|
||||
|
||||
apply_key_updates(&mut key, self.0)?;
|
||||
|
||||
garage.key_table.insert(&key).await?;
|
||||
|
||||
Ok(CreateKeyResponse(
|
||||
key_info_results(garage, key, true).await?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for ImportKeyRequest {
|
||||
type Response = ImportKeyResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<ImportKeyResponse, Error> {
|
||||
let prev_key = garage.key_table.get(&EmptyKey, &self.access_key_id).await?;
|
||||
if prev_key.is_some() {
|
||||
return Err(Error::KeyAlreadyExists(self.access_key_id.to_string()));
|
||||
}
|
||||
}
|
||||
if let Some(deny) = req.deny {
|
||||
if deny.create_bucket {
|
||||
key_state.allow_create_bucket.update(false);
|
||||
}
|
||||
}
|
||||
|
||||
garage.key_table.insert(&key).await?;
|
||||
let imported_key = Key::import(
|
||||
&self.access_key_id,
|
||||
&self.secret_access_key,
|
||||
self.name.as_deref().unwrap_or("Imported key"),
|
||||
)
|
||||
.ok_or_bad_request("Invalid key format")?;
|
||||
garage.key_table.insert(&imported_key).await?;
|
||||
|
||||
key_info_results(garage, key, false).await
|
||||
Ok(ImportKeyResponse(
|
||||
key_info_results(garage, imported_key, false).await?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UpdateKeyRequest {
|
||||
name: Option<String>,
|
||||
allow: Option<KeyPerm>,
|
||||
deny: Option<KeyPerm>,
|
||||
impl RequestHandler for UpdateKeyRequest {
|
||||
type Response = UpdateKeyResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<UpdateKeyResponse, Error> {
|
||||
let mut key = garage.key_helper().get_existing_key(&self.id).await?;
|
||||
|
||||
apply_key_updates(&mut key, self.body)?;
|
||||
|
||||
garage.key_table.insert(&key).await?;
|
||||
|
||||
Ok(UpdateKeyResponse(
|
||||
key_info_results(garage, key, false).await?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_delete_key(
|
||||
garage: &Arc<Garage>,
|
||||
id: String,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
let helper = garage.locked_helper().await;
|
||||
impl RequestHandler for DeleteKeyRequest {
|
||||
type Response = DeleteKeyResponse;
|
||||
|
||||
let mut key = helper.key().get_existing_key(&id).await?;
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<DeleteKeyResponse, Error> {
|
||||
let helper = garage.locked_helper().await;
|
||||
|
||||
helper.delete_key(&mut key).await?;
|
||||
let mut key = helper.key().get_existing_key(&self.id).await?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.body(empty_body())?)
|
||||
helper.delete_key(&mut key).await?;
|
||||
|
||||
Ok(DeleteKeyResponse)
|
||||
}
|
||||
}
|
||||
|
||||
async fn key_info_results(
|
||||
garage: &Arc<Garage>,
|
||||
key: Key,
|
||||
show_secret: bool,
|
||||
) -> Result<Response<ResBody>, Error> {
|
||||
) -> Result<GetKeyInfoResponse, Error> {
|
||||
let mut relevant_buckets = HashMap::new();
|
||||
|
||||
let key_state = key.state.as_option().unwrap();
|
||||
@@ -192,8 +210,15 @@ async fn key_info_results(
|
||||
}
|
||||
}
|
||||
|
||||
let res = GetKeyInfoResult {
|
||||
let res = GetKeyInfoResponse {
|
||||
name: key_state.name.get().clone(),
|
||||
created: key_state.created.map(|x| {
|
||||
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expiration: key_state.expiration.get().map(|x| {
|
||||
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
||||
}),
|
||||
expired: key_state.is_expired(now_msec()),
|
||||
access_key_id: key.key_id.clone(),
|
||||
secret_access_key: if show_secret {
|
||||
Some(key_state.secret_key.clone())
|
||||
@@ -205,9 +230,18 @@ async fn key_info_results(
|
||||
},
|
||||
buckets: relevant_buckets
|
||||
.into_values()
|
||||
.map(|bucket| {
|
||||
.filter_map(|bucket| {
|
||||
let state = bucket.state.as_option().unwrap();
|
||||
KeyInfoBucketResult {
|
||||
let permissions = key_state
|
||||
.authorized_buckets
|
||||
.get(&bucket.id)
|
||||
.filter(|p| p.is_any())
|
||||
.map(|p| ApiBucketKeyPerm {
|
||||
read: p.allow_read,
|
||||
write: p.allow_write,
|
||||
owner: p.allow_owner,
|
||||
})?;
|
||||
Some(KeyInfoBucketResponse {
|
||||
id: hex::encode(bucket.id),
|
||||
global_aliases: state
|
||||
.aliases
|
||||
@@ -223,57 +257,45 @@ async fn key_info_results(
|
||||
.filter(|((k, _), _, a)| *a && *k == key.key_id)
|
||||
.map(|((_, n), _, _)| n.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
permissions: key_state
|
||||
.authorized_buckets
|
||||
.get(&bucket.id)
|
||||
.map(|p| ApiBucketKeyPerm {
|
||||
read: p.allow_read,
|
||||
write: p.allow_write,
|
||||
owner: p.allow_owner,
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
permissions,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
|
||||
Ok(json_ok_response(&res)?)
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetKeyInfoResult {
|
||||
name: String,
|
||||
access_key_id: String,
|
||||
#[serde(skip_serializing_if = "is_default")]
|
||||
secret_access_key: Option<String>,
|
||||
permissions: KeyPerm,
|
||||
buckets: Vec<KeyInfoBucketResult>,
|
||||
}
|
||||
fn apply_key_updates(key: &mut Key, updates: UpdateKeyRequestBody) -> Result<(), Error> {
|
||||
if updates.never_expires && updates.expiration.is_some() {
|
||||
return Err(Error::bad_request(
|
||||
"cannot specify `expiration` and `never_expires`",
|
||||
));
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KeyPerm {
|
||||
#[serde(default)]
|
||||
create_bucket: bool,
|
||||
}
|
||||
let key_state = key.state.as_option_mut().unwrap();
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KeyInfoBucketResult {
|
||||
id: String,
|
||||
global_aliases: Vec<String>,
|
||||
local_aliases: Vec<String>,
|
||||
permissions: ApiBucketKeyPerm,
|
||||
}
|
||||
if let Some(new_name) = updates.name {
|
||||
key_state.name.update(new_name);
|
||||
}
|
||||
if let Some(expiration) = updates.expiration {
|
||||
key_state
|
||||
.expiration
|
||||
.update(Some(expiration.timestamp_millis() as u64));
|
||||
}
|
||||
if updates.never_expires {
|
||||
key_state.expiration.update(None);
|
||||
}
|
||||
if let Some(allow) = updates.allow {
|
||||
if allow.create_bucket {
|
||||
key_state.allow_create_bucket.update(true);
|
||||
}
|
||||
}
|
||||
if let Some(deny) = updates.deny {
|
||||
if deny.create_bucket {
|
||||
key_state.allow_create_bucket.update(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ApiBucketKeyPerm {
|
||||
#[serde(default)]
|
||||
pub(crate) read: bool,
|
||||
#[serde(default)]
|
||||
pub(crate) write: bool,
|
||||
#[serde(default)]
|
||||
pub(crate) owner: bool,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use garage_util::crdt::*;
|
||||
use garage_util::data::*;
|
||||
use garage_util::error::Error as GarageError;
|
||||
|
||||
use garage_rpc::layout;
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
use crate::{Admin, RequestHandler};
|
||||
|
||||
impl RequestHandler for GetClusterLayoutRequest {
|
||||
type Response = GetClusterLayoutResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<GetClusterLayoutResponse, Error> {
|
||||
Ok(format_cluster_layout(
|
||||
garage.system.cluster_layout().inner(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn format_cluster_layout(layout: &layout::LayoutHistory) -> GetClusterLayoutResponse {
|
||||
let current = layout.current();
|
||||
|
||||
let roles = current
|
||||
.roles
|
||||
.items()
|
||||
.iter()
|
||||
.filter_map(|(k, _, v)| v.0.clone().map(|x| (k, x)))
|
||||
.map(|(k, v)| {
|
||||
let stored_partitions = current.get_node_usage(k).ok().map(|x| x as u64);
|
||||
LayoutNodeRole {
|
||||
id: hex::encode(k),
|
||||
zone: v.zone.clone(),
|
||||
capacity: v.capacity,
|
||||
stored_partitions,
|
||||
usable_capacity: stored_partitions.map(|x| x * current.partition_size),
|
||||
tags: v.tags.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let staged_role_changes = layout
|
||||
.staging
|
||||
.get()
|
||||
.roles
|
||||
.items()
|
||||
.iter()
|
||||
.filter(|(k, _, v)| current.roles.get(k) != Some(v))
|
||||
.map(|(k, _, v)| match &v.0 {
|
||||
None => NodeRoleChange {
|
||||
id: hex::encode(k),
|
||||
action: NodeRoleChangeEnum::Remove { remove: true },
|
||||
},
|
||||
Some(r) => NodeRoleChange {
|
||||
id: hex::encode(k),
|
||||
action: NodeRoleChangeEnum::Update(NodeAssignedRole {
|
||||
zone: r.zone.clone(),
|
||||
capacity: r.capacity,
|
||||
tags: r.tags.clone(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let staged_parameters = if *layout.staging.get().parameters.get() != current.parameters {
|
||||
Some((*layout.staging.get().parameters.get()).into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
GetClusterLayoutResponse {
|
||||
version: current.version,
|
||||
roles,
|
||||
partition_size: current.partition_size,
|
||||
parameters: current.parameters.into(),
|
||||
staged_role_changes,
|
||||
staged_parameters,
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for GetClusterLayoutHistoryRequest {
|
||||
type Response = GetClusterLayoutHistoryResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<GetClusterLayoutHistoryResponse, Error> {
|
||||
let layout_helper = garage.system.cluster_layout();
|
||||
let layout = layout_helper.inner();
|
||||
let min_stored = layout.min_stored();
|
||||
|
||||
let versions = layout
|
||||
.versions
|
||||
.iter()
|
||||
.rev()
|
||||
.chain(layout.old_versions.iter().rev())
|
||||
.map(|ver| {
|
||||
let status = if ver.version == layout.current().version {
|
||||
ClusterLayoutVersionStatus::Current
|
||||
} else if ver.version >= min_stored {
|
||||
ClusterLayoutVersionStatus::Draining
|
||||
} else {
|
||||
ClusterLayoutVersionStatus::Historical
|
||||
};
|
||||
ClusterLayoutVersion {
|
||||
version: ver.version,
|
||||
status,
|
||||
storage_nodes: ver
|
||||
.roles
|
||||
.items()
|
||||
.iter()
|
||||
.filter(
|
||||
|(_, _, x)| matches!(x, layout::NodeRoleV(Some(c)) if c.capacity.is_some()),
|
||||
)
|
||||
.count() as u64,
|
||||
gateway_nodes: ver
|
||||
.roles
|
||||
.items()
|
||||
.iter()
|
||||
.filter(
|
||||
|(_, _, x)| matches!(x, layout::NodeRoleV(Some(c)) if c.capacity.is_none()),
|
||||
)
|
||||
.count() as u64,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let all_nodes = layout.get_all_nodes();
|
||||
let min_ack = layout_helper.ack_map_min();
|
||||
|
||||
let update_trackers = if layout.versions.len() > 1 {
|
||||
Some(
|
||||
all_nodes
|
||||
.iter()
|
||||
.map(|node| {
|
||||
(
|
||||
hex::encode(&node),
|
||||
NodeUpdateTrackers {
|
||||
ack: layout.update_trackers.ack_map.get(node, min_stored),
|
||||
sync: layout.update_trackers.sync_map.get(node, min_stored),
|
||||
sync_ack: layout.update_trackers.sync_ack_map.get(node, min_stored),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(GetClusterLayoutHistoryResponse {
|
||||
current_version: layout.current().version,
|
||||
min_ack,
|
||||
versions,
|
||||
update_trackers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
// ---- update functions ----
|
||||
|
||||
impl RequestHandler for UpdateClusterLayoutRequest {
|
||||
type Response = UpdateClusterLayoutResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<UpdateClusterLayoutResponse, Error> {
|
||||
let mut layout = garage.system.cluster_layout().inner().clone();
|
||||
|
||||
let mut roles = layout.current().roles.clone();
|
||||
roles.merge(&layout.staging.get().roles);
|
||||
|
||||
for change in self.roles {
|
||||
let node = hex::decode(&change.id).ok_or_bad_request("Invalid node identifier")?;
|
||||
let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?;
|
||||
|
||||
let new_role = match change.action {
|
||||
NodeRoleChangeEnum::Remove { remove: true } => None,
|
||||
NodeRoleChangeEnum::Update(NodeAssignedRole {
|
||||
zone,
|
||||
capacity,
|
||||
tags,
|
||||
}) => {
|
||||
if matches!(capacity, Some(cap) if cap < 1024) {
|
||||
return Err(Error::bad_request("Capacity should be at least 1K (1024)"));
|
||||
}
|
||||
Some(layout::NodeRole {
|
||||
zone,
|
||||
capacity,
|
||||
tags,
|
||||
})
|
||||
}
|
||||
_ => return Err(Error::bad_request("Invalid layout change")),
|
||||
};
|
||||
|
||||
layout
|
||||
.staging
|
||||
.get_mut()
|
||||
.roles
|
||||
.merge(&roles.update_mutator(node, layout::NodeRoleV(new_role)));
|
||||
}
|
||||
|
||||
if let Some(param) = self.parameters {
|
||||
if let ZoneRedundancy::AtLeast(r_int) = param.zone_redundancy {
|
||||
if r_int > layout.current().replication_factor {
|
||||
return Err(Error::bad_request(format!(
|
||||
"The zone redundancy must be smaller or equal to the replication factor ({}).",
|
||||
layout.current().replication_factor
|
||||
)));
|
||||
} else if r_int < 1 {
|
||||
return Err(Error::bad_request(
|
||||
"The zone redundancy must be at least 1.",
|
||||
));
|
||||
}
|
||||
}
|
||||
layout.staging.get_mut().parameters.update(param.into());
|
||||
}
|
||||
|
||||
garage
|
||||
.system
|
||||
.layout_manager
|
||||
.update_cluster_layout(&layout)
|
||||
.await?;
|
||||
|
||||
let res = format_cluster_layout(&layout);
|
||||
Ok(UpdateClusterLayoutResponse(res))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for PreviewClusterLayoutChangesRequest {
|
||||
type Response = PreviewClusterLayoutChangesResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<PreviewClusterLayoutChangesResponse, Error> {
|
||||
let layout = garage.system.cluster_layout().inner().clone();
|
||||
let new_ver = layout.current().version + 1;
|
||||
match layout.apply_staged_changes(new_ver) {
|
||||
Err(GarageError::Message(error)) => {
|
||||
Ok(PreviewClusterLayoutChangesResponse::Error { error })
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
Ok((new_layout, msg)) => Ok(PreviewClusterLayoutChangesResponse::Success {
|
||||
message: msg,
|
||||
new_layout: format_cluster_layout(&new_layout),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for ApplyClusterLayoutRequest {
|
||||
type Response = ApplyClusterLayoutResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<ApplyClusterLayoutResponse, Error> {
|
||||
let layout = garage.system.cluster_layout().inner().clone();
|
||||
let (layout, msg) = layout.apply_staged_changes(self.version)?;
|
||||
|
||||
garage
|
||||
.system
|
||||
.layout_manager
|
||||
.update_cluster_layout(&layout)
|
||||
.await?;
|
||||
|
||||
Ok(ApplyClusterLayoutResponse {
|
||||
message: msg,
|
||||
layout: format_cluster_layout(&layout),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for RevertClusterLayoutRequest {
|
||||
type Response = RevertClusterLayoutResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<RevertClusterLayoutResponse, Error> {
|
||||
let layout = garage.system.cluster_layout().inner().clone();
|
||||
let layout = layout.revert_staged_changes()?;
|
||||
garage
|
||||
.system
|
||||
.layout_manager
|
||||
.update_cluster_layout(&layout)
|
||||
.await?;
|
||||
|
||||
let res = format_cluster_layout(&layout);
|
||||
Ok(RevertClusterLayoutResponse(res))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for ClusterLayoutSkipDeadNodesRequest {
|
||||
type Response = ClusterLayoutSkipDeadNodesResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<ClusterLayoutSkipDeadNodesResponse, Error> {
|
||||
let status = garage.system.get_known_nodes();
|
||||
|
||||
let mut layout = garage.system.cluster_layout().inner().clone();
|
||||
let mut ack_updated = vec![];
|
||||
let mut sync_updated = vec![];
|
||||
|
||||
if layout.versions.len() == 1 {
|
||||
return Err(Error::bad_request(
|
||||
"This command cannot be called when there is only one live cluster layout version",
|
||||
));
|
||||
}
|
||||
|
||||
let min_v = layout.min_stored();
|
||||
if self.version <= min_v || self.version > layout.current().version {
|
||||
return Err(Error::bad_request(format!(
|
||||
"Invalid version, you may use the following version numbers: {}",
|
||||
(min_v + 1..=layout.current().version)
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
)));
|
||||
}
|
||||
|
||||
let all_nodes = layout.get_all_nodes();
|
||||
for node in all_nodes.iter() {
|
||||
// Update ACK tracker for dead nodes or for all nodes if --allow-missing-data
|
||||
if self.allow_missing_data || !status.iter().any(|x| x.id == *node && x.is_up) {
|
||||
if layout.update_trackers.ack_map.set_max(*node, self.version) {
|
||||
ack_updated.push(hex::encode(node));
|
||||
}
|
||||
}
|
||||
|
||||
// If --allow-missing-data, update SYNC tracker for all nodes.
|
||||
if self.allow_missing_data {
|
||||
if layout.update_trackers.sync_map.set_max(*node, self.version) {
|
||||
sync_updated.push(hex::encode(node));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
garage
|
||||
.system
|
||||
.layout_manager
|
||||
.update_cluster_layout(&layout)
|
||||
.await?;
|
||||
|
||||
Ok(ClusterLayoutSkipDeadNodesResponse {
|
||||
ack_updated,
|
||||
sync_updated,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
impl From<layout::ZoneRedundancy> for ZoneRedundancy {
|
||||
fn from(x: layout::ZoneRedundancy) -> Self {
|
||||
match x {
|
||||
layout::ZoneRedundancy::Maximum => ZoneRedundancy::Maximum,
|
||||
layout::ZoneRedundancy::AtLeast(x) => ZoneRedundancy::AtLeast(x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<layout::ZoneRedundancy> for ZoneRedundancy {
|
||||
fn into(self) -> layout::ZoneRedundancy {
|
||||
match self {
|
||||
ZoneRedundancy::Maximum => layout::ZoneRedundancy::Maximum,
|
||||
ZoneRedundancy::AtLeast(x) => layout::ZoneRedundancy::AtLeast(x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<layout::LayoutParameters> for LayoutParameters {
|
||||
fn from(x: layout::LayoutParameters) -> Self {
|
||||
LayoutParameters {
|
||||
zone_redundancy: x.zone_redundancy.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<layout::LayoutParameters> for LayoutParameters {
|
||||
fn into(self) -> layout::LayoutParameters {
|
||||
layout::LayoutParameters {
|
||||
zone_redundancy: self.zone_redundancy.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||