Compare commits

...

84 commits

Author SHA1 Message Date
4c5be79b80 Garage v0.8.5
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
This minor release includes the following improvements and fixes:
 
 New features:
 
 - Configuration: make LMDB's `map_size` configurable and make `block_size` and `sled_cache_capacity` expressable as strings (such as `10M`) (#628, #630)
 
 - Add support for binding to Unix sockets for the S3, K2V, Admin and Web API servers (#640)
 
 - Move the `convert_db` command into the main Garage binary (#645)
 
 - Add support for specifying RPC secret and admin tokens as environment variables (#643)
 
 - Add `allow_world_readable_secrets` option to config file (#663, #685)
 
 Bug fixes:
 
 - Use `statvfs` instead of mount list to determine free space in metadata/data directories (#611, #631)
 
 - Add missing casts to fix 32-bit build (#632)
 
 - Fix error when none of the HTTP servers (S3/K2V/Admin/Web) is started and fix shutdown hang (#613, #633)
 
 - Add missing CORS headers to PostObject response (#609, #656)
 
 - Monitoring: finer histogram boundaries in Prometheus exported metrics (#531, #686)
 
 Other:
 
 - Documentation improvements (#641)
 -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEEwhSWp0+ubv79TiqUDkltFQljdr4FAmWmWvsACgkQDkltFQlj
 dr59rRAAiMGQpDUK0QqiCgrp1rcUhvtj3DsQEpT7F14Jo3I7bFDmONZolPbO8YAs
 VE4S4CBQogNH0lMQ6EvJYiBCxDWkxdVibKqDWOYJmUw3bZ6Ypn1eZIF0+Uf1TDI+
 C6CxYbyDQtqvm330K2Du2uOoGiIgm83b6jktK/0FtbAE2GWhtYmQwoelprAGH20i
 baaSfkZbBl8toUscakyhPVVSQ86BcVQ2jqL6Ofu4eQknjMRqCeAIQhMB2ikpiwBz
 hbTZ3x0EfJJqiHocfkTE3B3cPnDKuHDzxPRhLMB/olEpzoxaLJ2+tc0ziQdl06/F
 1c8nHM57L1IaDGKAkpcANnj3yVf3jfPqq9SEUNi+xSIWbvln91RvXU4RIB8hiZqa
 rqAHjDuys++3DoAUr/L4X233MWufVAEYT4B+jaPAv6ys35xhQwPAMJrA0OZEr+hE
 HQMPIG9uMDVjZ2QCgFYgC02kEqvxbsRSVnb0wjI7eoNOk0LKo154eJh1cOGd4Ibs
 yBTiIi1+Y7RCXNxcIHKlj5vMUHPBr2D8DVFj21kfZKUtMQ/8yScoiRC14ZR4J2xF
 IYe3aDm80l3tYgnPRVj4fOGiIPsqnZd4iazYKwj2cifB8tzYfyh5/9fv2aio8K5y
 0GAw4AoTtgg1hLMadbc3om7wy64IRaZzXjv59eYPEotZYdreVpM=
 =RVm8
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEEwhSWp0+ubv79TiqUDkltFQljdr4FAmWmZKIACgkQDkltFQlj
 dr6wZRAA1XuOBax/7YsIix3ag0kjnwnGAx8wYaA+Jiojw2yv/+ePL6yGHcKA93lI
 SL8l5G06fTDgpbpfdVbgyRzGT2tmjrXvkygRWf2WMDZ9I+8WxUA2q8aWaEMiNmvd
 0cfzYi14TgX+O0wEbKeeqrXG0473/yThk5U1FNbdJd7rkJ4JzaOTthdk0LJLiEUG
 zQ/YIYx3FVFoVI0rdORb3HKzqYHjMAvpzNhEIeqkrpDEzplQ3jKvY+rYWQL3S9zE
 bHbkZPoT62OpJGMr04/1FUkB+ctsvUrM0CskruaSKWyD2M1xTo/Ug4jh5muVIcdJ
 hJis1/k5rV8JDTIkb6eAxKqfVzI+56yDxofT8rVF4JhvlzvXDLOa0uyDVyA8/6un
 ylWRzs2Mlj6/TbscmPjrdH8v2Lb0zjWxvXe2iYnHHfldWUlYuBtI6FZiG3uNjBCs
 7ns3xr4VOw13RM5auVkEQksIO6lru0kvH18GB3h6Msx67w2JUzl+PaNv8PdRtnmV
 0SfLUl1Nh8yT2h9qG6/3cDE9E1G/mjg8SgljoEe6ahs/BUZmLuTHTyBjf+P22ZbO
 DCITM3CwrV+y/aKnRdLvd6LOWFinUqMS8YvVSVqJh9vo9R+dt33LdBMdWjP4IYHF
 MbACe4FzeG3AXUcHB/mDCm7a2H2BFwzAovFy0SE639PfWBxNue0=
 =gzWq
 -----END PGP SIGNATURE-----

Merge tag 'v0.8.5' into sync-08-09

Garage v0.8.5

This minor release includes the following improvements and fixes:

New features:

- Configuration: make LMDB's `map_size` configurable and make `block_size` and `sled_cache_capacity` expressable as strings (such as `10M`) (#628, #630)

- Add support for binding to Unix sockets for the S3, K2V, Admin and Web API servers (#640)

- Move the `convert_db` command into the main Garage binary (#645)

- Add support for specifying RPC secret and admin tokens as environment variables (#643)

- Add `allow_world_readable_secrets` option to config file (#663, #685)

Bug fixes:

- Use `statvfs` instead of mount list to determine free space in metadata/data directories (#611, #631)

- Add missing casts to fix 32-bit build (#632)

- Fix error when none of the HTTP servers (S3/K2V/Admin/Web) is started and fix shutdown hang (#613, #633)

- Add missing CORS headers to PostObject response (#609, #656)

- Monitoring: finer histogram boundaries in Prometheus exported metrics (#531, #686)

Other:

- Documentation improvements (#641)
2024-01-16 12:12:27 +01:00
083e982f5f Merge pull request 'Garage v0.8.5' (#687) from rel-0.8.5 into main-0.8.x
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #687
2024-01-16 10:30:54 +00:00
50643e61bf
Bump version to 0.8.5
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-01-16 10:47:33 +01:00
a6421ee5a5 Merge pull request 'monitoring: finer histogram boundaries in prometheus metrics (fix #531)' (#686) from fix-531 into main-0.8.x
All checks were successful
continuous-integration/drone Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #686
2024-01-15 16:44:58 +00:00
993ce74976 Merge pull request '0.8.x: config: refactor secret sourcing' (#685) from secret-sourcing into main-0.8.x
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #685
2024-01-15 16:41:50 +00:00
f512609123
monitoring: finer histogram boundaries in prometheus metrics (fix #531)
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-15 17:33:35 +01:00
97bae7213a
config: additional tests for secret sourcing
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-15 17:30:30 +01:00
7228695ee2
config: refactor secret sourcing
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-15 17:18:46 +01:00
ee7fe27d3d Merge pull request 'Add allow_world_readable_secrets option to config file' (#663) from PicNoir/garage:nin/world-readable-conf-file into main-0.8.x
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #663
2024-01-15 15:20:16 +00:00
d91a1de731 Merge pull request 'fix typo in peertube doc' (#617) from Lapineige/garage:main into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #617
2024-01-11 11:19:42 +00:00
723e56b37f Merge pull request 'Jepsen testing (NLnet task 3 subtask 1)' (#544) from jepsen into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #544
2024-01-11 10:52:12 +00:00
fa9247f11b jepsen: updated results, confirming that task3 works
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-12-14 16:23:48 +01:00
a8b0e01f88 Merge pull request 'OpenAPI specification of admin APIv1' (#672) from api-v1 into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #672
2023-11-29 15:42:46 +00:00
8088690650
fix the doc
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-28 16:18:28 +01:00
ffa659433d Merge pull request 'Doc: fix db_engines section and improve config reference' (#674) from fix-doc-db-engine into main
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #674
2023-11-28 12:03:46 +00:00
cfa5550cb2 doc: move replication_mode to top of configuration page reference
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-28 11:58:27 +01:00
939d1f2e17 doc: improve navigation in configuration reference 2023-11-28 11:53:26 +01:00
1f6efe57be doc: update the db_engine section 2023-11-28 11:33:31 +01:00
3908619eac
add ClusterHealthReport endpoint to the API
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-28 09:34:01 +01:00
68d23cccdf
disable int64 finally for now
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-23 10:20:36 +01:00
9f1043586c
set layout version as required
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-23 10:16:16 +01:00
1caa6e29e5
capacity is int64
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-23 10:02:41 +01:00
814b3e11d4
fix query parameters for keys
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-23 08:50:10 +01:00
2d37e7fa39
convert showsecretkey from bool to enum
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-22 21:05:36 +01:00
4f473f43c9
Change how query parameters are handled
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2023-11-22 20:39:38 +01:00
3684c29ad0
handle key changes
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-22 18:14:38 +01:00
0d415f42ac
Port GetKeyInfo by adding showSecretKey query param
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-22 18:05:11 +01:00
20b3afbde4
Port layout endpoints
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-22 17:49:51 +01:00
e3cd6ed530
port GetLayout and AddLayout
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2023-11-22 15:24:30 +01:00
9b24d7c402
Upgrade GetNodes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-22 14:25:04 +01:00
36bd21a148 Merge pull request 'Allow 0 as a part number marker' (#670) from asonix/garage:main into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #670
2023-11-22 10:33:31 +00:00
d1d1940252
Health info message now advertises API v1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-22 09:28:50 +01:00
c63b446989
skeleton for api v1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-22 08:58:09 +01:00
92fd899fb6 Allow 0 as a part number marker
All checks were successful
continuous-integration/drone/pr Build is passing
2023-11-21 17:39:51 -06:00
92dd2bbe15 jepsen: nlnet task3a seems to fix things
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-16 18:09:13 +01:00
18e5811159
jepsen: add patch and use more complete names
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2023-11-16 12:57:21 +01:00
f83fa02193 Add allow_world_readable_secrets option to config file
All checks were successful
continuous-integration/drone/pr Build is passing
Sometimes, the secret files permissions checks gets in the way. It's
by no mean complete, it doesn't take the Posix ACLs into account among
other things. Correctly checking the ACLs would be too involving (see
#658 (comment))
and would likely still fail in some weird chmod settings.

We're adding a new configuration file key allowing the user to disable
this permission check altogether.

The (already existing) env variable counterpart always take precedence
to this config file option. That's useful in cases where the
configuration file is static and cannot be easily altered.

Fixes #658

Co-authored-by: Florian Klink <flokli@flokli.de>
2023-10-26 18:25:13 +02:00
4b3dee2ca3 Merge pull request 's3 api: add missing CORS headers to PostObject responses (fix #609)' (#656) from fix-cors-post-object into main-0.8.x
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #656
2023-10-26 09:17:14 +00:00
5b1f50be65 jepsen: testing
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-25 14:43:24 +02:00
9df7fa0bcd jepsen: use 7 nodes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-25 14:04:39 +02:00
fd85010a40 jepsen: failures with set2 test in --scenario r
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-25 12:13:27 +02:00
cfbfa09d24 jepsen: fix set2 test omg finally this is so stupid 2023-10-25 11:50:16 +02:00
db921cc05f jepsen: reconfigure nemesis + add db nemesis 2023-10-25 11:41:34 +02:00
4fa2646a75 jepsen: got a failure with set1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-24 17:45:22 +02:00
d7ab2c639e jepsen: fix nemesis to actually generate many operations
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-24 16:39:50 +02:00
d13bde5e26 jepsen: set1 and set2 don't fail anymore ??
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-24 15:44:05 +02:00
d2c365767b jepsen: more testing
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-24 11:39:45 +02:00
fb6c9a1243 jepsen: update readme
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-20 15:55:09 +02:00
9030c1eef8 jepsen: code path for nemesis final generator
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-20 15:53:46 +02:00
654775308e jepsen: add cluster reconfiguration nemesis
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-20 15:48:37 +02:00
f5b0972781 jepsen: register crdt read-after-write is fixed with deleteobject patch
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-20 15:00:10 +02:00
d148b83d4f jepsen: reg2 failure seems to happen only with deleteobject
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-20 13:36:48 +02:00
4b93ce179a jepsen: errors in reg2 workload under investigation
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-20 12:56:55 +02:00
4ba18ce9cc jepsen: wip checker for register-like behavior
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-20 12:13:11 +02:00
ac04934dae s3 api: add missing CORS headers to PostObject responses (fix #609)
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-10-20 10:37:48 +02:00
ef662822c9 jepsen: fix the list-objects call (?)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-19 23:40:55 +02:00
da8b170748 jepsen: investigating listobjects error
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-19 16:45:24 +02:00
74e50edddd jepsen: refactoring 2023-10-19 14:34:19 +02:00
0215b11402 Merge pull request 'Add support for specifying rpc_secret_file, metrics_token_file and admin_token_file using environment variables' (#643) from networkException/garage:token-file-env into main-0.8.x
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #643
2023-10-19 09:33:12 +00:00
8599051c49
garage: support specifying token / secret as environment variables
All checks were successful
continuous-integration/drone/pr Build is passing
this patch adds support for specifying the `rpc_secret_file`,
`metrics_token_file` and `admin_token_file` as environment variables.
2023-10-19 03:39:02 +02:00
4a19ee94bb
garage: fix admin-token description 2023-10-19 03:31:50 +02:00
c99cb58d71
util: move reading secret file into seperate helper
this patch moves the logic to read a secret file (and check for correct
permissions) from `secret_from_file` into a new `read_secret_file`
helper.
2023-10-19 03:29:48 +02:00
5feb6a1f64
docs: add documentation for specifying token / secret file as environment variables 2023-10-19 03:28:44 +02:00
b3bf16ee27 make jepsen test more robust: handle errors and timeouts, fixed access key
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-18 17:51:34 +02:00
ddd3de7fce refactor jepsen code
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-18 16:30:45 +02:00
84d43501ce refactor jepsen setup logic
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-18 15:34:12 +02:00
012ade5d4b jepsen: update jepsen and fix garage key info 2023-10-18 14:06:32 +02:00
ef5ca86dfc jepsen: update to garage 0.9.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-18 14:01:18 +02:00
9ec4cca334 reformatting
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-10-18 12:03:12 +02:00
18ee8efb5f Check read-after-write property for sets 2023-10-18 12:03:12 +02:00
55eb4e87c4 set tests with independant tests together 2023-10-18 12:03:11 +02:00
0bb1577ae1 two set workloads with different checkers 2023-10-18 12:03:11 +02:00
6eb26be548 Add garage set test (this one works :p) 2023-10-18 12:03:11 +02:00
eb86eaa6d2 refactor jepsen test 2023-10-18 12:03:11 +02:00
80d7b7d858 remove useless files 2023-10-18 12:03:11 +02:00
93a7132b4c the fix for increasing timestamps does not make things linearizable 2023-10-18 12:03:11 +02:00
dc5245ce65 even without nemesis, s3 get/put/delete is not linearizable (is this normal?) 2023-10-18 12:03:11 +02:00
70c1d3db46 better match exceptions 2023-10-18 12:03:11 +02:00
bc11701999 jepsen: s3 gets and puts 2023-10-18 12:03:11 +02:00
ca4cc7e44f jepsen connects to vagrant vms 2023-10-18 12:03:11 +02:00
17ebb65273 jepsen ssh into containers seem to work ? 2023-10-18 12:03:11 +02:00
7011b71fbd jepsen: wip 2023-10-18 12:03:11 +02:00
47e7f9e122 another typo
All checks were successful
continuous-integration/drone/pr Build is passing
2023-08-19 20:29:24 +00:00
5ffcdb4634 fix typo
All checks were successful
continuous-integration/drone/pr Build is passing
2023-08-19 15:17:51 +00:00
44 changed files with 3130 additions and 388 deletions

1
Cargo.lock generated
View file

@ -1227,6 +1227,7 @@ dependencies = [
"hyper", "hyper",
"k2v-client", "k2v-client",
"kuska-sodiumoxide", "kuska-sodiumoxide",
"mktemp",
"netapp", "netapp",
"opentelemetry", "opentelemetry",
"opentelemetry-otlp", "opentelemetry-otlp",

View file

@ -33,7 +33,7 @@ args@{
ignoreLockHash, ignoreLockHash,
}: }:
let let
nixifiedLockHash = "1a87886681a3ef0b83c95addc26674a538b8a93d35bc80db8998e1fcd0821f6c"; nixifiedLockHash = "a8ba32879366acd517e5c5feb96e9b009e1dc1991b51fb3acbb6dba2e5c0d935";
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc; workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock); currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
lockHashIgnored = if ignoreLockHash lockHashIgnored = if ignoreLockHash
@ -1771,6 +1771,7 @@ in
http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.9" { inherit profileName; }).out; http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.9" { inherit profileName; }).out;
hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.27" { inherit profileName; }).out; hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.27" { inherit profileName; }).out;
k2v_client = (rustPackages."unknown".k2v-client."0.0.4" { inherit profileName; }).out; k2v_client = (rustPackages."unknown".k2v-client."0.0.4" { inherit profileName; }).out;
mktemp = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".mktemp."0.5.0" { inherit profileName; }).out;
serde_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.105" { inherit profileName; }).out; serde_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.105" { inherit profileName; }).out;
sha2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.10.7" { inherit profileName; }).out; sha2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.10.7" { inherit profileName; }).out;
static_init = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init."1.0.3" { inherit profileName; }).out; static_init = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".static_init."1.0.3" { inherit profileName; }).out;

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>Garage Adminstration API v0</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="./css/redoc.css" rel="stylesheet">
<!--
Redoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='./garage-admin-v1.yml'></redoc>
<script src="./redoc.standalone.js"> </script>
</body>
</html>

1363
doc/api/garage-admin-v1.yml Normal file

File diff suppressed because it is too large Load diff

View file

@ -37,30 +37,84 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"strings"
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
) )
func main() { func main() {
// Set Host and other parameters // Initialization
configuration := garage.NewConfiguration() configuration := garage.NewConfiguration()
configuration.Host = "127.0.0.1:3903" configuration.Host = "127.0.0.1:3903"
// We can now generate a client
client := garage.NewAPIClient(configuration) client := garage.NewAPIClient(configuration)
// Authentication is handled through the context pattern
ctx := context.WithValue(context.Background(), garage.ContextAccessToken, "s3cr3t") ctx := context.WithValue(context.Background(), garage.ContextAccessToken, "s3cr3t")
// Send a request // Nodes
resp, r, err := client.NodesApi.GetNodes(ctx).Execute() fmt.Println("--- nodes ---")
if err != nil { nodes, _, _ := client.NodesApi.GetNodes(ctx).Execute()
fmt.Fprintf(os.Stderr, "Error when calling `NodesApi.GetNodes``: %v\n", err) fmt.Fprintf(os.Stdout, "First hostname: %v\n", nodes.KnownNodes[0].Hostname)
fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) capa := int64(1000000000)
change := []garage.NodeRoleChange{
garage.NodeRoleChange{NodeRoleUpdate: &garage.NodeRoleUpdate {
Id: *nodes.KnownNodes[0].Id,
Zone: "dc1",
Capacity: *garage.NewNullableInt64(&capa),
Tags: []string{ "fast", "amd64" },
}},
} }
staged, _, _ := client.LayoutApi.AddLayout(ctx).NodeRoleChange(change).Execute()
msg, _, _ := client.LayoutApi.ApplyLayout(ctx).LayoutVersion(*garage.NewLayoutVersion(staged.Version + 1)).Execute()
fmt.Printf(strings.Join(msg.Message, "\n")) // Layout configured
// Process the response health, _, _ := client.NodesApi.GetHealth(ctx).Execute()
fmt.Fprintf(os.Stdout, "Target hostname: %v\n", resp.KnownNodes[resp.Node].Hostname) fmt.Printf("Status: %s, nodes: %v/%v, storage: %v/%v, partitions: %v/%v\n", health.Status, health.ConnectedNodes, health.KnownNodes, health.StorageNodesOk, health.StorageNodes, health.PartitionsAllOk, health.Partitions)
// Key
fmt.Println("\n--- key ---")
key := "openapi-key"
keyInfo, _, _ := client.KeyApi.AddKey(ctx).AddKeyRequest(garage.AddKeyRequest{Name: *garage.NewNullableString(&key) }).Execute()
defer client.KeyApi.DeleteKey(ctx).Id(*keyInfo.AccessKeyId).Execute()
fmt.Printf("AWS_ACCESS_KEY_ID=%s\nAWS_SECRET_ACCESS_KEY=%s\n", *keyInfo.AccessKeyId, *keyInfo.SecretAccessKey.Get())
id := *keyInfo.AccessKeyId
canCreateBucket := true
updateKeyRequest := *garage.NewUpdateKeyRequest()
updateKeyRequest.SetName("openapi-key-updated")
updateKeyRequest.SetAllow(garage.UpdateKeyRequestAllow { CreateBucket: &canCreateBucket })
update, _, _ := client.KeyApi.UpdateKey(ctx).Id(id).UpdateKeyRequest(updateKeyRequest).Execute()
fmt.Printf("Updated %v with key name %v\n", *update.AccessKeyId, *update.Name)
keyList, _, _ := client.KeyApi.ListKeys(ctx).Execute()
fmt.Printf("Keys count: %v\n", len(keyList))
// Bucket
fmt.Println("\n--- bucket ---")
global_name := "global-ns-openapi-bucket"
local_name := "local-ns-openapi-bucket"
bucketInfo, _, _ := client.BucketApi.CreateBucket(ctx).CreateBucketRequest(garage.CreateBucketRequest{
GlobalAlias: &global_name,
LocalAlias: &garage.CreateBucketRequestLocalAlias {
AccessKeyId: keyInfo.AccessKeyId,
Alias: &local_name,
},
}).Execute()
defer client.BucketApi.DeleteBucket(ctx).Id(*bucketInfo.Id).Execute()
fmt.Printf("Bucket id: %s\n", *bucketInfo.Id)
updateBucketRequest := *garage.NewUpdateBucketRequest()
website := garage.NewUpdateBucketRequestWebsiteAccess()
website.SetEnabled(true)
website.SetIndexDocument("index.html")
website.SetErrorDocument("errors/4xx.html")
updateBucketRequest.SetWebsiteAccess(*website)
quotas := garage.NewUpdateBucketRequestQuotas()
quotas.SetMaxSize(1000000000)
quotas.SetMaxObjects(999999999)
updateBucketRequest.SetQuotas(*quotas)
updatedBucket, _, _ := client.BucketApi.UpdateBucket(ctx).Id(*bucketInfo.Id).UpdateBucketRequest(updateBucketRequest).Execute()
fmt.Printf("Bucket %v website activation: %v\n", *updatedBucket.Id, *updatedBucket.WebsiteAccess)
bucketList, _, _ := client.BucketApi.ListBuckets(ctx).Execute()
fmt.Printf("Bucket count: %v\n", len(bucketList))
} }
``` ```

View file

@ -31,9 +31,9 @@ npm install --save git+https://git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-js.
A short example: A short example:
```javascript ```javascript
const garage = require('garage_administration_api_v0garage_v0_8_0'); const garage = require('garage_administration_api_v1garage_v0_9_0');
const api = new garage.ApiClient("http://127.0.0.1:3903/v0"); const api = new garage.ApiClient("http://127.0.0.1:3903/v1");
api.authentications['bearerAuth'].accessToken = "s3cr3t"; api.authentications['bearerAuth'].accessToken = "s3cr3t";
const [node, layout, key, bucket] = [ const [node, layout, key, bucket] = [

View file

@ -80,7 +80,7 @@ from garage_admin_sdk.apis import *
from garage_admin_sdk.models import * from garage_admin_sdk.models import *
configuration = garage_admin_sdk.Configuration( configuration = garage_admin_sdk.Configuration(
host = "http://localhost:3903/v0", host = "http://localhost:3903/v1",
access_token = "s3cr3t" access_token = "s3cr3t"
) )
@ -94,13 +94,14 @@ print(f"running garage {status.garage_version}, node_id {status.node}")
# Change layout of this node # Change layout of this node
current = layout.get_layout() current = layout.get_layout()
layout.add_layout({ layout.add_layout([
status.node: NodeClusterInfo( NodeRoleChange(
id = status.node,
zone = "dc1", zone = "dc1",
capacity = 1, capacity = 1000000000,
tags = [ "dev" ], tags = [ "dev" ],
) )
}) ])
layout.apply_layout(LayoutVersion( layout.apply_layout(LayoutVersion(
version = current.version + 1 version = current.version + 1
)) ))

View file

@ -146,7 +146,7 @@ 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-video) and one for webtorrent videos (named peertube-playlist).
```bash ```bash
garage bucket create peertube-video garage bucket create peertube-videos
garage bucket create peertube-playlist garage bucket create peertube-playlist
``` ```
@ -216,7 +216,7 @@ object_storage:
# Same settings but for webtorrent videos # Same settings but for webtorrent videos
videos: videos:
bucket_name: 'peertube-video' bucket_name: 'peertube-videos'
prefix: '' prefix: ''
# You must fill this field to make Peertube use our reverse proxy/website logic # You must fill this field to make Peertube use our reverse proxy/website logic
base_url: 'http://peertube-videos.web.garage.localhost' base_url: 'http://peertube-videos.web.garage.localhost'

View file

@ -38,7 +38,7 @@ Our website serving logic is as follow:
Now we need to infer the URL of your website through your bucket name. Now we need to infer the URL of your website through your bucket name.
Let assume: Let assume:
- we set `root_domain = ".web.example.com"` in `garage.toml` ([ref](@/documentation/reference-manual/configuration.md#root_domain)) - we set `root_domain = ".web.example.com"` in `garage.toml` ([ref](@/documentation/reference-manual/configuration.md#web_root_domain))
- our bucket name is `garagehq.deuxfleurs.fr`. - our bucket name is `garagehq.deuxfleurs.fr`.
Our bucket will be served if the Host field matches one of these 2 values (the port is ignored): Our bucket will be served if the Host field matches one of these 2 values (the port is ignored):

View file

@ -12,7 +12,7 @@ An introduction to building cluster layouts can be found in the [production depl
In Garage, all of the data that can be stored in a given cluster is divided In Garage, all of the data that can be stored in a given cluster is divided
into slices which we call *partitions*. Each partition is stored by into slices which we call *partitions*. Each partition is stored by
one or several nodes in the cluster one or several nodes in the cluster
(see [`replication_mode`](@/documentation/reference-manual/configuration.md#replication-mode)). (see [`replication_mode`](@/documentation/reference-manual/configuration.md#replication_mode)).
The layout determines the correspondence between these partition, The layout determines the correspondence between these partition,
which exist on a logical level, and actual storage nodes. which exist on a logical level, and actual storage nodes.

View file

@ -13,8 +13,11 @@ We will bump the version numbers prefixed to each API endpoint at each time the
or semantics change, meaning that code that relies on these endpoint will break or semantics change, meaning that code that relies on these endpoint will break
when changes are introduced. when changes are introduced.
The Garage administration API was introduced in version 0.7.2, this document Versions:
does not apply to older versions of Garage. - Before Garage 0.7.2 - no admin API
- Garage 0.7.2 - admin APIv0
- Garage 0.9.0 - admin APIv1, deprecate admin APIv0
## Access control ## Access control
@ -131,7 +134,9 @@ $ curl -so /dev/null -w "%{http_code}" http://localhost:3903/check?domain=exampl
### Cluster operations ### Cluster operations
These endpoints are defined on a dedicated [Redocly page](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.html). You can also download its [OpenAPI specification](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.yml). 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: Requesting the API from the command line can be as simple as running:

View file

@ -8,6 +8,8 @@ weight = 20
Here is an example `garage.toml` configuration file that illustrates all of the possible options: Here is an example `garage.toml` configuration file that illustrates all of the possible options:
```toml ```toml
replication_mode = "3"
metadata_dir = "/var/lib/garage/meta" metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data" data_dir = "/var/lib/garage/data"
metadata_fsync = true metadata_fsync = true
@ -21,8 +23,6 @@ sled_cache_capacity = "128MiB"
sled_flush_every_ms = 2000 sled_flush_every_ms = 2000
lmdb_map_size = "1T" lmdb_map_size = "1T"
replication_mode = "3"
compression_level = 1 compression_level = 1
rpc_secret = "4425f5c26c5e11581d3223904324dcb5b5d5dfb14e5e7f35e38c595424f5f1e6" rpc_secret = "4425f5c26c5e11581d3223904324dcb5b5d5dfb14e5e7f35e38c595424f5f1e6"
@ -77,157 +77,64 @@ The following gives details about each available configuration option.
## Available configuration options ## Available configuration options
### `metadata_dir` ### Index
The directory in which Garage will store its metadata. This contains the node identifier, Top-level configuration options:
the network configuration and the peer list, the list of buckets and keys as well [`block_size`](#block_size),
as the index of all objects, object version and object blocks. [`bootstrap_peers`](#bootstrap_peers),
[`compression_level`](#compression_level),
[`data_dir`](#metadata_dir),
[`data_fsync`](#data_fsync),
[`db_engine`](#db_engine),
[`lmdb_map_size`](#lmdb_map_size),
[`metadata_dir`](#metadata_dir),
[`metadata_fsync`](#metadata_fsync),
[`replication_mode`](#replication_mode),
[`rpc_bind_addr`](#rpc_bind_addr),
[`rpc_public_addr`](#rpc_public_addr),
[`rpc_secret`](#rpc_secret),
[`rpc_secret_file`](#rpc_secret),
[`sled_cache_capacity`](#sled_cache_capacity),
[`sled_flush_every_ms`](#sled_flush_every_ms).
Store this folder on a fast SSD drive if possible to maximize Garage's performance. The `[consul_discovery]` section:
[`api`](#consul_api),
[`ca_cert`](#consul_ca_cert),
[`client_cert`](#consul_client_cert),
[`client_key`](#consul_client_cert),
[`consul_http_addr`](#consul_http_addr),
[`meta`](#consul_tags),
[`service_name`](#consul_service_name),
[`tags`](#consul_tags),
[`tls_skip_verify`](#consul_tls_skip_verify),
[`token`](#consul_token).
### `data_dir` The `[kubernetes_discovery]` section:
[`namespace`](#kube_namespace),
[`service_name`](#kube_service_name),
[`skip_crd`](#kube_skip_crd).
The directory in which Garage will store the data blocks of objects. The `[s3_api]` section:
This folder can be placed on an HDD. The space available for `data_dir` [`api_bind_addr`](#s3_api_bind_addr),
should be counted to determine a node's capacity [`root_domain`](#s3_root_domain),
when [adding it to the cluster layout](@/documentation/cookbook/real-world.md). [`s3_region`](#s3_region).
Since `v0.9.0`, Garage supports multiple data directories with the following syntax: The `[s3_web]` section:
[`bind_addr`](#web_bind_addr),
[`root_domain`](#web_root_domain).
```toml The `[admin]` section:
data_dir = [ [`api_bind_addr`](#admin_api_bind_addr),
{ path = "/path/to/old_data", read_only = true }, [`metrics_token`](#admin_metrics_token),
{ path = "/path/to/new_hdd1", capacity = "2T" }, [`metrics_token_file`](#admin_metrics_token),
{ path = "/path/to/new_hdd2", capacity = "4T" }, [`admin_token`](#admin_token),
] [`admin_token_file`](#admin_token),
``` [`trace_sink`](#admin_trace_sink),
See [the dedicated documentation page](@/documentation/operations/multi-hdd.md)
on how to operate Garage in such a setup.
### `db_engine` (since `v0.8.0`) ### Top-level configuration options
By default, Garage uses the Sled embedded database library #### `replication_mode` {#replication_mode}
to store its metadata on-disk. Since `v0.8.0`, Garage can use alternative storage backends as follows:
| DB engine | `db_engine` value | Database path |
| --------- | ----------------- | ------------- |
| [Sled](https://sled.rs) | `"sled"` | `<metadata_dir>/db/` |
| [LMDB](https://www.lmdb.tech) | `"lmdb"` | `<metadata_dir>/db.lmdb/` |
| [Sqlite](https://sqlite.org) | `"sqlite"` | `<metadata_dir>/db.sqlite` |
Performance characteristics of the different DB engines are as follows:
- Sled: the default database engine, which tends to produce
large data files and also has performance issues, especially when the metadata folder
is on a traditional HDD and not on SSD.
- LMDB: the recommended alternative on 64-bit systems,
much more space-efficiant and slightly faster. Note that 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 node. Also note that, while LMDB can technically be used on 32-bit systems,
this will limit your node to very small database sizes due to how LMDB works; it is therefore not recommended.
- Sqlite: Garage supports Sqlite as a storage backend for metadata,
however it may have issues and is also very slow in its current implementation,
so it is not recommended to be used for now.
It is possible to convert Garage's metadata directory from one format to another with a small utility named `convert_db`,
which can be downloaded at the following locations:
[for amd64](https://garagehq.deuxfleurs.fr/_releases/convert_db/amd64/convert_db),
[for i386](https://garagehq.deuxfleurs.fr/_releases/convert_db/i386/convert_db),
[for arm64](https://garagehq.deuxfleurs.fr/_releases/convert_db/arm64/convert_db),
[for arm](https://garagehq.deuxfleurs.fr/_releases/convert_db/arm/convert_db).
The `convert_db` utility is used as folows:
```
convert-db -a <input db engine> -i <input db path> \
-b <output db engine> -o <output db path>
```
Make sure to specify the full database path as presented in the table above,
and not just the path to the metadata directory.
### `metadata_fsync`
Whether to enable synchronous mode for the database engine or not.
This is disabled (`false`) by default.
This reduces the risk of metadata corruption in case of power failures,
at the cost of a significant drop in write performance,
as Garage will have to pause to sync data to disk much more often
(several times for API calls such as PutObject).
Using this option reduces the risk of simultaneous metadata corruption on several
cluster nodes, which could lead to data loss.
If multi-site replication is used, this option is most likely not necessary, as
it is extremely unlikely that two nodes in different locations will have a
power failure at the exact same time.
(Metadata corruption on a single node is not an issue, the corrupted data file
can always be deleted and reconstructed from the other nodes in the cluster.)
Here is how this option impacts the different database engines:
| Database | `metadata_fsync = false` (default) | `metadata_fsync = true` |
|----------|------------------------------------|-------------------------------|
| Sled | default options | *unsupported* |
| Sqlite | `PRAGMA synchronous = OFF` | `PRAGMA synchronous = NORMAL` |
| LMDB | `MDB_NOMETASYNC` + `MDB_NOSYNC` | `MDB_NOMETASYNC` |
Note that the Sqlite database is always ran in `WAL` mode (`PRAGMA journal_mode = WAL`).
### `data_fsync`
Whether to `fsync` data blocks and their containing directory after they are
saved to disk.
This is disabled (`false`) by default.
This might reduce the risk that a data block is lost in rare
situations such as simultaneous node losing power,
at the cost of a moderate drop in write performance.
Similarly to `metatada_fsync`, this is likely not necessary
if geographical replication is used.
### `block_size`
Garage splits stored objects in consecutive chunks of size `block_size`
(except the last one which might be smaller). The default size is 1MiB and
should work in most cases. We recommend increasing it to e.g. 10MiB if
you are using Garage to store large files and have fast network connections
between all nodes (e.g. 1gbps).
If you are interested in tuning this, feel free to do so (and remember to
report your findings to us!). When this value is changed for a running Garage
installation, only files newly uploaded will be affected. Previously uploaded
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.
### `sled_cache_capacity`
This parameter can be used to tune the capacity of the cache used by
[sled](https://sled.rs), the database Garage uses internally to store metadata.
Tune this to fit the RAM you wish to make available to your Garage instance.
This value has a conservative default (128MB) so that Garage doesn't use too much
RAM by default, but feel free to increase this for higher performance.
### `sled_flush_every_ms`
This parameters can be used to tune the flushing interval of sled.
Increase this if sled is thrashing your SSD, at the risk of losing more data in case
of a power outage (though this should not matter much as data is replicated on other
nodes). The default value, 2000ms, should be appropriate for most use cases.
### `lmdb_map_size`
This parameters can be used to set the map size used by LMDB,
which is the size of the virtual memory region used for mapping the database file.
The value of this parameter is the maximum size the metadata database can take.
This value is not bound by the physical RAM size of the machine running Garage.
If not specified, it defaults to 1GiB on 32-bit machines and 1TiB on 64-bit machines.
### `replication_mode`
Garage supports the following replication modes: Garage supports the following replication modes:
@ -310,7 +217,160 @@ to the cluster while rebalancing is in progress. In theory, no data should be
lost as rebalancing is a routine operation for Garage, although we cannot lost as rebalancing is a routine operation for Garage, although we cannot
guarantee you that everything will go right in such an extreme scenario. guarantee you that everything will go right in such an extreme scenario.
### `compression_level` #### `metadata_dir` {#metadata_dir}
The directory in which Garage will store its metadata. This contains the node identifier,
the network configuration and the peer list, the list of buckets and keys as well
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.
This folder can be placed on an HDD. The space available for `data_dir`
should be counted to determine a node's capacity
when [adding it to the cluster layout](@/documentation/cookbook/real-world.md).
Since `v0.9.0`, Garage supports multiple data directories with the following syntax:
```toml
data_dir = [
{ path = "/path/to/old_data", read_only = true },
{ path = "/path/to/new_hdd1", capacity = "2T" },
{ path = "/path/to/new_hdd2", capacity = "4T" },
]
```
See [the dedicated documentation page](@/documentation/operations/multi-hdd.md)
on how to operate Garage in such a setup.
#### `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) (default since `v0.9.0`) | `"lmdb"` | `<metadata_dir>/db.lmdb/` |
| [Sled](https://sled.rs) (default up to `v0.8.0`) | `"sled"` | `<metadata_dir>/db/` |
| [Sqlite](https://sqlite.org) | `"sqlite"` | `<metadata_dir>/db.sqlite` |
Sled was the only database engine up to Garage v0.7.0. Performance issues and
API limitations of Sled prompted the addition of alternative engines in v0.8.0.
Since v0.9.0, LMDB is the default engine instead of Sled, and Sled is
deprecated. We plan to remove Sled in Garage v1.0.
Performance characteristics of the different DB engines are as follows:
- Sled: tends to produce large data files and also has performance issues,
especially when the metadata folder is on a traditional HDD and not on SSD.
- LMDB: the recommended database engine on 64-bit systems, much more
space-efficient and slightly faster. Note that 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 node. Also note that, while LMDB can
technically be used on 32-bit systems, this will limit your node to very
small database sizes due to how LMDB works; it is therefore not recommended.
- Sqlite: Garage supports Sqlite as an alternative storage backend for
metadata, and although it has not been tested as much, it is expected to work
satisfactorily. Since Garage v0.9.0, performance issues have largely been
fixed by allowing for a no-fsync mode (see `metadata_fsync`). Sqlite does not
have the database size limitation of LMDB on 32-bit systems.
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:
```
garage convert-db -a <input db engine> -i <input db path> \
-b <output db engine> -o <output db path>
```
Make sure to specify the full database path as presented in the table above
(third colummn), and not just the path to the metadata directory.
#### `metadata_fsync` {#metadata_fsync}
Whether to enable synchronous mode for the database engine or not.
This is disabled (`false`) by default.
This reduces the risk of metadata corruption in case of power failures,
at the cost of a significant drop in write performance,
as Garage will have to pause to sync data to disk much more often
(several times for API calls such as PutObject).
Using this option reduces the risk of simultaneous metadata corruption on several
cluster nodes, which could lead to data loss.
If multi-site replication is used, this option is most likely not necessary, as
it is extremely unlikely that two nodes in different locations will have a
power failure at the exact same time.
(Metadata corruption on a single node is not an issue, the corrupted data file
can always be deleted and reconstructed from the other nodes in the cluster.)
Here is how this option impacts the different database engines:
| Database | `metadata_fsync = false` (default) | `metadata_fsync = true` |
|----------|------------------------------------|-------------------------------|
| Sled | default options | *unsupported* |
| Sqlite | `PRAGMA synchronous = OFF` | `PRAGMA synchronous = NORMAL` |
| LMDB | `MDB_NOMETASYNC` + `MDB_NOSYNC` | `MDB_NOMETASYNC` |
Note that the Sqlite database is always ran in `WAL` mode (`PRAGMA journal_mode = WAL`).
#### `data_fsync` {#data_fsync}
Whether to `fsync` data blocks and their containing directory after they are
saved to disk.
This is disabled (`false`) by default.
This might reduce the risk that a data block is lost in rare
situations such as simultaneous node losing power,
at the cost of a moderate drop in write performance.
Similarly to `metatada_fsync`, this is likely not necessary
if geographical replication is used.
#### `block_size` {#block_size}
Garage splits stored objects in consecutive chunks of size `block_size`
(except the last one which might be smaller). The default size is 1MiB and
should work in most cases. We recommend increasing it to e.g. 10MiB if
you are using Garage to store large files and have fast network connections
between all nodes (e.g. 1gbps).
If you are interested in tuning this, feel free to do so (and remember to
report your findings to us!). When this value is changed for a running Garage
installation, only files newly uploaded will be affected. Previously uploaded
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.
#### `sled_cache_capacity` {#sled_cache_capacity}
This parameter can be used to tune the capacity of the cache used by
[sled](https://sled.rs), the database Garage uses internally to store metadata.
Tune this to fit the RAM you wish to make available to your Garage instance.
This value has a conservative default (128MB) so that Garage doesn't use too much
RAM by default, but feel free to increase this for higher performance.
#### `sled_flush_every_ms` {#sled_flush_every_ms}
This parameters can be used to tune the flushing interval of sled.
Increase this if sled is thrashing your SSD, at the risk of losing more data in case
of a power outage (though this should not matter much as data is replicated on other
nodes). The default value, 2000ms, should be appropriate for most use cases.
#### `lmdb_map_size` {#lmdb_map_size}
This parameters can be used to set the map size used by LMDB,
which is the size of the virtual memory region used for mapping the database file.
The value of this parameter is the maximum size the metadata database can take.
This value is not bound by the physical RAM size of the machine running Garage.
If not specified, it defaults to 1GiB on 32-bit machines and 1TiB on 64-bit machines.
#### `compression_level` {#compression_level}
Zstd compression level to use for storing blocks. Zstd compression level to use for storing blocks.
@ -334,7 +394,7 @@ Compression is done synchronously, setting a value too high will add latency to
This value can be different between nodes, compression is done by the node which receive the This value can be different between nodes, compression is done by the node which receive the
API call. API call.
### `rpc_secret`, `rpc_secret_file` or `GARAGE_RPC_SECRET` (env) #### `rpc_secret`, `rpc_secret_file` or `GARAGE_RPC_SECRET`, `GARAGE_RPC_SECRET_FILE` (env) {#rpc_secret}
Garage uses a secret key, called an RPC secret, that is shared between all Garage uses a secret key, called an RPC secret, that is shared between all
nodes of the cluster in order to identify these nodes and allow them to nodes of the cluster in order to identify these nodes and allow them to
@ -346,7 +406,10 @@ Since Garage `v0.8.2`, the RPC secret can also be stored in a file whose path is
given in the configuration variable `rpc_secret_file`, or specified as an given in the configuration variable `rpc_secret_file`, or specified as an
environment variable `GARAGE_RPC_SECRET`. environment variable `GARAGE_RPC_SECRET`.
### `rpc_bind_addr` Since Garage `v0.8.5` and `v0.9.1`, you can also specify the path of a file
storing the secret as the `GARAGE_RPC_SECRET_FILE` environment variable.
#### `rpc_bind_addr` {#rpc_bind_addr}
The address and port on which to bind for inter-cluster communcations The address and port on which to bind for inter-cluster communcations
(reffered to as RPC for remote procedure calls). (reffered to as RPC for remote procedure calls).
@ -355,14 +418,14 @@ 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 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. behind a NAT, they should each use a different RPC port number.
### `rpc_public_addr` #### `rpc_public_addr` {#rpc_public_addr}
The address and port that other nodes need to use to contact this node for The address and port that other nodes need to use to contact this node for
RPC calls. **This parameter is optional but recommended.** In case you have 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, a NAT that binds the RPC port to a port that is different on your public IP,
this field might help making it work. this field might help making it work.
### `bootstrap_peers` #### `bootstrap_peers` {#bootstrap_peers}
A list of peer identifiers on which to contact other Garage peers of this cluster. A list of peer identifiers on which to contact other Garage peers of this cluster.
These peer identifiers have the following syntax: These peer identifiers have the following syntax:
@ -378,43 +441,54 @@ 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 key will be returned by `garage node id` and you will have to add the IP
yourself. yourself.
### `allow_world_readable_secrets`
## The `[consul_discovery]` section 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
world-readable even if they're not, for instance when using Posix ACLs.
Setting `allow_world_readable_secrets` to `true` bypass this
permission verification.
Alternatively, you can set the `GARAGE_ALLOW_WORLD_READABLE_SECRETS`
environment variable to `true` to bypass the permissions check.
### The `[consul_discovery]` section
Garage supports discovering other nodes of the cluster using Consul. For this Garage supports discovering other nodes of the cluster using Consul. For this
to work correctly, nodes need to know their IP address by which they can be to work correctly, nodes need to know their IP address by which they can be
reached by other nodes of the cluster, which should be set in `rpc_public_addr`. reached by other nodes of the cluster, which should be set in `rpc_public_addr`.
### `consul_http_addr` and `service_name` #### `consul_http_addr` {#consul_http_addr}
The `consul_http_addr` parameter should be set to the full HTTP(S) address of the Consul server. The `consul_http_addr` parameter should be set to the full HTTP(S) address of the Consul server.
### `api` #### `api` {#consul_api}
Two APIs for service registration are supported: `catalog` and `agent`. `catalog`, the default, will register a service using Two APIs for service registration are supported: `catalog` and `agent`. `catalog`, the default, will register a service using
the `/v1/catalog` endpoints, enabling mTLS if `client_cert` and `client_key` are provided. The `agent` API uses the the `/v1/catalog` endpoints, enabling mTLS if `client_cert` and `client_key` are provided. The `agent` API uses the
`v1/agent` endpoints instead, where an optional `token` may be provided. `v1/agent` endpoints instead, where an optional `token` may be provided.
### `service_name` #### `service_name` {#consul_service_name}
`service_name` should be set to the service name under which Garage's `service_name` should be set to the service name under which Garage's
RPC ports are announced. RPC ports are announced.
### `client_cert`, `client_key` #### `client_cert`, `client_key` {#consul_client_cert}
TLS client certificate and client key to use when communicating with Consul over TLS. Both are mandatory when doing so. TLS client certificate and client key to use when communicating with Consul over TLS. Both are mandatory when doing so.
Only available when `api = "catalog"`. Only available when `api = "catalog"`.
### `ca_cert` #### `ca_cert` {#consul_ca_cert}
TLS CA certificate to use when communicating with Consul over TLS. TLS CA certificate to use when communicating with Consul over TLS.
### `tls_skip_verify` #### `tls_skip_verify` {#consul_tls_skip_verify}
Skip server hostname verification in TLS handshake. Skip server hostname verification in TLS handshake.
`ca_cert` is ignored when this is set. `ca_cert` is ignored when this is set.
### `token` #### `token` {#consul_token}
Uses the provided token for communication with Consul. Only available when `api = "agent"`. Uses the provided token for communication with Consul. Only available when `api = "agent"`.
The policy assigned to this token should at least have these rules: The policy assigned to this token should at least have these rules:
@ -434,49 +508,49 @@ node_prefix "" {
} }
``` ```
### `tags` and `meta` #### `tags` and `meta` {#consul_tags}
Additional list of tags and map of service meta to add during service registration. Additional list of tags and map of service meta to add during service registration.
## The `[kubernetes_discovery]` section ### The `[kubernetes_discovery]` section
Garage supports discovering other nodes of the cluster using kubernetes custom Garage supports discovering other nodes of the cluster using kubernetes custom
resources. For this to work, a `[kubernetes_discovery]` section must be present resources. For this to work, a `[kubernetes_discovery]` section must be present
with at least the `namespace` and `service_name` parameters. with at least the `namespace` and `service_name` parameters.
### `namespace` #### `namespace` {#kube_namespace}
`namespace` sets the namespace in which the custom resources are `namespace` sets the namespace in which the custom resources are
configured. configured.
### `service_name` #### `service_name` {#kube_service_name}
`service_name` is added as a label to the advertised resources to `service_name` is added as a label to the advertised resources to
filter them, to allow for multiple deployments in a single namespace. filter them, to allow for multiple deployments in a single namespace.
### `skip_crd` #### `skip_crd` {#kube_skip_crd}
`skip_crd` can be set to true to disable the automatic creation and `skip_crd` can be set to true to disable the automatic creation and
patching of the `garagenodes.deuxfleurs.fr` CRD. You will need to create the CRD patching of the `garagenodes.deuxfleurs.fr` CRD. You will need to create the CRD
manually. manually.
## The `[s3_api]` section ### The `[s3_api]` section
### `api_bind_addr` #### `api_bind_addr` {#s3_api_bind_addr}
The IP and port on which to bind for accepting S3 API calls. The IP and port on which to bind for accepting S3 API calls.
This endpoint does not suport TLS: a reverse proxy should be used to provide it. This endpoint does not suport TLS: a reverse proxy should be used to provide it.
Alternatively, since `v0.8.5`, a path can be used to create a unix socket with 0222 mode. Alternatively, since `v0.8.5`, a path can be used to create a unix socket with 0222 mode.
### `s3_region` #### `s3_region` {#s3_region}
Garage will accept S3 API calls that are targetted to the S3 region defined here. Garage will accept S3 API calls that are targetted to the S3 region defined here.
API calls targetted to other regions will fail with a AuthorizationHeaderMalformed error API calls targetted to other regions will fail with a AuthorizationHeaderMalformed error
message that redirects the client to the correct region. message that redirects the client to the correct region.
### `root_domain` {#root_domain} #### `root_domain` {#s3_root_domain}
The optional suffix to access bucket using vhost-style in addition to path-style request. The optional suffix to access bucket using vhost-style in addition to path-style request.
Note path-style requests are always enabled, whether or not vhost-style is configured. Note path-style requests are always enabled, whether or not vhost-style is configured.
@ -488,12 +562,12 @@ using the hostname `my-bucket.s3.garage.eu`.
## The `[s3_web]` section ### The `[s3_web]` section
Garage allows to publish content of buckets as websites. This section configures the Garage allows to publish content of buckets as websites. This section configures the
behaviour of this module. behaviour of this module.
### `bind_addr` #### `bind_addr` {#web_bind_addr}
The IP and port on which to bind for accepting HTTP requests to buckets configured The IP and port on which to bind for accepting HTTP requests to buckets configured
for website access. for website access.
@ -501,7 +575,7 @@ This endpoint does not suport TLS: a reverse proxy should be used to provide it.
Alternatively, since `v0.8.5`, a path can be used to create a unix socket with 0222 mode. Alternatively, since `v0.8.5`, a path can be used to create a unix socket with 0222 mode.
### `root_domain` #### `root_domain` {#web_root_domain}
The optional suffix appended to bucket names for the corresponding HTTP Host. The optional suffix appended to bucket names for the corresponding HTTP Host.
@ -510,11 +584,11 @@ will be accessible either with hostname `deuxfleurs.fr.web.garage.eu`
or with hostname `deuxfleurs.fr`. or with hostname `deuxfleurs.fr`.
## The `[admin]` section ### The `[admin]` section
Garage has a few administration capabilities, in particular to allow remote monitoring. These features are detailed below. Garage has a few administration capabilities, in particular to allow remote monitoring. These features are detailed below.
### `api_bind_addr` #### `api_bind_addr` {#admin_api_bind_addr}
If specified, Garage will bind an HTTP server to this port and address, on If specified, Garage will bind an HTTP server to this port and address, on
which it will listen to requests for administration features. which it will listen to requests for administration features.
@ -523,7 +597,7 @@ 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, 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. the socket will have 0220 mode. Make sure to set user and group permissions accordingly.
### `metrics_token`, `metrics_token_file` or `GARAGE_METRICS_TOKEN` (env) #### `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 The token for accessing the Metrics endpoint. If this token is not set, the
Metrics endpoint can be accessed without access control. Metrics endpoint can be accessed without access control.
@ -533,8 +607,9 @@ You can use any random string for this value. We recommend generating a random t
`metrics_token` was introduced in Garage `v0.7.2`. `metrics_token` was introduced in Garage `v0.7.2`.
`metrics_token_file` and the `GARAGE_METRICS_TOKEN` environment variable are supported since Garage `v0.8.2`. `metrics_token_file` and the `GARAGE_METRICS_TOKEN` environment variable are supported since Garage `v0.8.2`.
`GARAGE_METRICS_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN` (env) #### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
The token for accessing all of the other administration endpoints. If this The token for accessing all of the other administration endpoints. If this
token is not set, access to these endpoints is disabled entirely. token is not set, access to these endpoints is disabled entirely.
@ -544,8 +619,9 @@ You can use any random string for this value. We recommend generating a random t
`admin_token` was introduced in Garage `v0.7.2`. `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`. `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`.
### `trace_sink` #### `trace_sink` {#admin_trace_sink}
Optionally, the address of an OpenTelemetry collector. If specified, Optionally, the address of an OpenTelemetry collector. If specified,
Garage will send traces in the OpenTelemetry format to this endpoint. These Garage will send traces in the OpenTelemetry format to this endpoint. These

View file

@ -52,7 +52,7 @@ This is particularly usefull when nodes are far from one another and talk to one
Garage supports a variety of replication modes, with 1 copy, 2 copies or 3 copies of your data, Garage supports a variety of replication modes, with 1 copy, 2 copies or 3 copies of your data,
and with various levels of consistency, in order to adapt to a variety of usage scenarios. and with various levels of consistency, in order to adapt to a variety of usage scenarios.
Read our reference page on [supported replication modes](@/documentation/reference-manual/configuration.md#replication-mode) Read our reference page on [supported replication modes](@/documentation/reference-manual/configuration.md#replication_mode)
to select the replication mode best suited to your use case (hint: in most cases, `replication_mode = "3"` is what you want). to select the replication mode best suited to your use case (hint: in most cases, `replication_mode = "3"` is what you want).
### Web server for static websites ### Web server for static websites

View file

@ -0,0 +1 @@
use nix

17
script/jepsen.garage/.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
/target
/classes
/checkouts
profiles.clj
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
/.prepl-port
.hgignore
.hg/
.direnv
/store
/store.*
.vagrant

View file

@ -0,0 +1,166 @@
# jepsen.garage
Jepsen checking of Garage consistency properties.
## Usage
Requirements:
- vagrant
- VirtualBox, configured so that nodes can take an IP in a private network `192.168.56.0/24` (it's the default)
- a user that can create VirtualBox VMs
- leiningen
- gnuplot
Set up VMs before running tests:
```
vagrant up
```
Run tests: see commands below.
## Results
### Register linear, without timestamp patch
Command: `lein run test --nodes-file nodes.vagrant --time-limit 60 --rate 100 --concurrency 20 --workload reg1 --ops-per-key 100`
Results without timestamp patch:
- Fails with a simple clock-scramble nemesis (`--scenario c`).
Explanation: without the timestamp patch, nodes will create objects using their
local clock only as a timestamp, so the ordering will be all over the place if
clocks are scrambled.
Results with timestamp patch (`--patch tsfix2`):
- No failure with clock-scramble nemesis
- Fails with clock-scramble nemesis + partition nemesis (`--scenario cp`).
**This test is expected to fail.**
Indeed, S3 objects are not meant to behave like linearizable registers.
TODO explain using a counter-example
### Read-after-write CRDT register model
Command: `lein run test --nodes-file nodes.vagrant --time-limit 60 --rate 100 --concurrency 100 --workload reg2 --ops-per-key 100`
Results without timestamp patch:
- Fails with a simple clock-scramble nemesis (`--scenario c`).
Explanation: old values are not overwritten correctly when their timestamps are in the future.
Results with timestamp patch (`--patch tsfix2`):
- No failures with clock-scramble nemesis + partition nemesis (`--scenario cp`).
This proves that `tsfix2` (PR#543) does improve consistency.
- **Fails with layout reconfiguration nemesis** (`--scenario r`).
Example of a failed run: `garage reg2/20231024T120806.899+0200`.
This is the failure mode we are looking for and trying to fix for NLnet task 3.
Results with NLnet task 3 code (commit 707442f5de, `--patch task3a`):
- No failures with `--scenario r` (0 of 10 runs), `--scenario pr` (0 of 10 runs),
`--scenario cpr` (0 of 10 runs) and `--scenario dpr` (0 of 10 runs).
- Same with `--patch task3c` (commit `0041b013`, the final version).
### Set, basic test (write some items, then read)
Command: `lein run test --nodes-file nodes.vagrant --time-limit 60 --rate 200 --concurrency 200 --workload set1 --ops-per-key 100`
Results without NLnet task3 code (`--patch tsfix2`):
- For now, no failures with clock-scramble nemesis + partition nemesis -> TODO long test run
- Does not seem to fail with only the layout reconfiguation nemesis (<10 runs), although theoretically it could
- **Fails with the partition + layout reconfiguration nemesis** (`--scenario pr`).
Example of a failed run: `garage set1/20231024T172214.488+0200` (1 failure in 4 runs).
This is the failure mode we are looking for and trying to fix for NLnet task 3.
Results with NLnet task 3 code (commit 707442f5de, `--patch task3a`):
- The tests are buggy and often result in an "unknown" validity status, which
is caused by some requests not returning results during network partitions or
other nemesis-induced broken cluster states. However, when the tests were
able to finish, there were no failures with scenarios `r`, `pr`, `cpr`,
`dpr`.
### Set, continuous test (interspersed reads and writes)
Command: `lein run test --nodes-file nodes.vagrant --time-limit 60 --rate 100 --concurrency 100 --workload set2 --ops-per-key 100`
Results without NLnet task3 code (`--patch tsfix2`):
- No failures with clock-scramble nemesis + db nemesis + partition nemesis (`--scenario cdp`) (0 failures in 10 runs).
- **Fails with just layout reconfiguration nemesis** (`--scenario r`).
Example of a failed run: `garage set2/20231025T141940.198+0200` (10 failures in 10 runs).
This is the failure mode we are looking for and trying to fix for NLnet task 3.
Results with NLnet task3 code (commit 707442f5de, `--patch task3a`):
- No failures with `--scenario r` (0 of 10 runs), `--scenario pr` (0 of 10 runs),
`--scenario cpr` (0 of 10 runs) and `--scenario dpr` (0 of 10 runs).
- Same with `--patch task3c` (commit `0041b013`, the final version).
## NLnet task 3 final results
- With code from task3 (`--patch task3c`): [reg2 and set2](results/Results-2023-12-13-task3c.png), [set1](results/Results-2023-12-14-task3-set1.png).
- Without (`--patch tsfix2`): [reg2 and set2](results/Results-2023-12-13-tsfix2.png), set1 TBD.
## Investigating (and fixing) errors
### Segfaults
They are due to the download being interrupted in the middle (^C during first launch on clean VMs), the `garage` binary is truncated.
Add `:force?` to the `cached-wget!` call in `daemon.clj` to re-download the binary,
or restar the VMs to clear temporary files.
### In `jepsen.garage`: prefix wierdness
In `store/garage set1/20231019T163358.615+0200`:
```
INFO [2023-10-19 16:35:20,977] clojure-agent-send-off-pool-207 - jepsen.garage.set list results for prefix set20/ : (set13/0 set13/1 set13/10 set13/11 set13/12 set13/13 set13/14 set13/15 set13/16 set13/17 set13/18 set13/19 set13/2 set13/20 set13/21 set13/22 set13/23 set13/24 set13/25 set13/26 set13/27 set13/28 set13/29 set13/3 set13/30 set13/31 set13/32 set13/33 set13/34 set13/35 set13/36 set13/37 set13/38 set13/39 set13/4 set13/40 set13/41 set13/42 set13/43 set13/44 set13/45 set13/46 set13/47 set13/48 set13/49 set13/5 set13/50 set13/51 set13/52 set13/53 set13/54 set13/55 set13/56 set13/57 set13/58 set13/59 set13/6 set13/60 set13/61 set13/62 set13/63 set13/64 set13/65 set13/66 set13/67 set13/68 set13/69 set13/7 set13/70 set13/71 set13/72 set13/73 set13/74 set13/75 set13/76 set13/77 set13/78 set13/79 set13/8 set13/80 set13/81 set13/82 set13/83 set13/84 set13/85 set13/86 set13/87 set13/88 set13/89 set13/9 set13/90 set13/91 set13/92 set13/93 set13/94 set13/95 set13/96 set13/97 set13/98 set13/99) (node: http://192.168.56.25:3900 )
```
After inspecting, the actual S3 call made was with prefix "set13/", so at least this is not an error in Garage itself but in the jepsen code.
Finally found out that this was due to closures not correctly capturing their context in the list function in s3api.clj (wtf clojure?)
Not sure exactly where it came from but it seems to have been fixed by making list-inner a separate function and not a sub-function,
and passing all values that were previously in the context (creds and prefix) as additional arguments.
### `reg2` test inconsistency, even with timestamp fix
The reg2 test is our custom checker for CRDT read-after-write on individual object keys, acting as registers which can be updated.
The test fails without the timestamp fix, which is expected as the clock scrambler will prevent nodes from having a correct ordering of objects.
With the timestamp fix (`--patch tsfix1`), the happenned-before relationship should at least be respected, meaning that when a PutObject call starts
after another PutObject call has ended, the second call should overwrite the value of the first call, and that value should not be
readable by future GetObject calls.
However, we observed inconsistencies even with the timestamp fix.
The inconsistencies seemed to always happenned after writing a nil value, which translates to a DeleteObject call
instead of a PutObject. By removing the possibility of writing nil values, therefore only doing
PutObject calls, the issue disappears. There is therefore an issue to fix in DeleteObject.
The issue in DeleteObject seems to have been fixed by commit `c82d91c6bccf307186332b6c5c6fc0b128b1b2b1`, which can be used using `--patch tsfix2`.
## License
Copyright © 2023 Alex Auvolat
This program and the accompanying materials are made available under the
terms of the GNU Affero General Public License v3.0.

40
script/jepsen.garage/Vagrantfile vendored Normal file
View file

@ -0,0 +1,40 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
#
def vm(config, hostname, ip)
config.vm.hostname = hostname
config.vm.network "private_network", ip: ip
end
Vagrant.configure("2") do |config|
config.vm.box = "generic/debian10"
config.vm.provider "virtualbox" do |vb|
vb.gui = false
vb.memory = "512"
vb.customize ["modifyvm", :id, "--vram=12"]
end
config.vm.provision "shell", inline: <<-SHELL
echo "root:root" | chpasswd
mkdir -p /root/.ssh
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJpaBZdYxHqMxhv2RExAOa7nkKhPBOHupMP3mYaZ73w9 lx@lindy" >> /root/.ssh/authorized_keys
SHELL
config.vm.define "n1" do |config| vm(config, "n1", "192.168.56.21") end
config.vm.define "n2" do |config| vm(config, "n2", "192.168.56.22") end
config.vm.define "n3" do |config| vm(config, "n3", "192.168.56.23") end
config.vm.define "n4" do |config| vm(config, "n4", "192.168.56.24") end
config.vm.define "n5" do |config| vm(config, "n5", "192.168.56.25") end
config.vm.define "n6" do |config| vm(config, "n6", "192.168.56.26") end
config.vm.define "n7" do |config| vm(config, "n7", "192.168.56.27") end
config.vm.define "n8" do |config| vm(config, "n8", "192.168.56.28") end
config.vm.define "n9" do |config| vm(config, "n9", "192.168.56.29") end
config.vm.define "n10" do |config| vm(config, "n10", "192.168.56.30") end
config.vm.define "n11" do |config| vm(config, "n11", "192.168.56.31") end
config.vm.define "n12" do |config| vm(config, "n12", "192.168.56.32") end
config.vm.define "n13" do |config| vm(config, "n13", "192.168.56.33") end
config.vm.define "n14" do |config| vm(config, "n14", "192.168.56.34") end
end

View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -x
#for ppatch in task3c task3a tsfix2; do
for ppatch in tsfix2; do
#for psc in c cp cdp r pr cpr dpr; do
for psc in cdp r pr cpr dpr; do
#for ptsk in reg2 set1 set2; do
for ptsk in set1; do
for irun in $(seq 10); do
lein run test --nodes-file nodes.vagrant \
--time-limit 60 --rate 100 --concurrency 100 --ops-per-key 100 \
--workload $ptsk --patch $ppatch --scenario $psc
done
done
done
done

View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -x
#for ppatch in task3c tsfix2; do
for ppatch in tsfix2; do
for psc in cdp r pr cpr dpr; do
for ptsk in set1; do
for irun in $(seq 10); do
lein run test --nodes-file nodes2.vagrant \
--time-limit 60 --rate 100 --concurrency 100 --ops-per-key 100 \
--workload $ptsk --patch $ppatch --scenario $psc
done
done
done
done

View file

@ -0,0 +1,13 @@
docker stop jaeger
docker rm jaeger
# UI is on localhost:16686
# otel-grpc collector is on localhost:4317
# otel-http collector is on localhost:4318
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 4317:4317 \
-p 4318:4318 \
-p 16686:16686 \
jaegertracing/all-in-one:1.50

View file

@ -0,0 +1,7 @@
192.168.56.21
192.168.56.22
192.168.56.23
192.168.56.24
192.168.56.25
192.168.56.26
192.168.56.27

View file

@ -0,0 +1,7 @@
192.168.56.28
192.168.56.29
192.168.56.30
192.168.56.31
192.168.56.32
192.168.56.33
192.168.56.34

View file

@ -0,0 +1,10 @@
(defproject jepsen.garage "0.1.0-SNAPSHOT"
:description "Jepsen testing for Garage"
:url "https://git.deuxfleurs.fr/Deuxfleurs/garage"
:license {:name "AGPLv3"
:url "https://www.gnu.org/licenses/agpl-3.0.en.html"}
:main jepsen.garage
:dependencies [[org.clojure/clojure "1.11.1"]
[jepsen "0.3.3-SNAPSHOT"]
[amazonica "0.3.163"]]
:repl-options {:init-ns jepsen.garage})

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

View file

@ -0,0 +1,18 @@
{ pkgs ? import <nixpkgs> {
overlays = [
(self: super: {
jdk = super.jdk11;
jre = super.jre11;
})
];
} }:
pkgs.mkShell {
nativeBuildInputs = with pkgs; [
leiningen
jdk
jna
vagrant
gnuplot
graphviz
];
}

View file

@ -0,0 +1,105 @@
(ns jepsen.garage
(:require
[clojure.string :as str]
[jepsen
[checker :as checker]
[cli :as cli]
[generator :as gen]
[nemesis :as nemesis]
[tests :as tests]]
[jepsen.os.debian :as debian]
[jepsen.garage
[daemon :as grg]
[nemesis :as grgNemesis]
[reg :as reg]
[set :as set]]))
(def workloads
"A map of workload names to functions that construct workloads, given opts."
{"reg1" reg/workload1
"reg2" reg/workload2
"set1" set/workload1
"set2" set/workload2})
(def scenari
"A map of scenari to the associated nemesis"
{"c" grgNemesis/scenario-c
"cp" grgNemesis/scenario-cp
"r" grgNemesis/scenario-r
"pr" grgNemesis/scenario-pr
"cpr" grgNemesis/scenario-cpr
"cdp" grgNemesis/scenario-cdp
"dpr" grgNemesis/scenario-dpr})
(def patches
"A map of patch names to Garage builds"
{"default" "v0.9.0"
"tsfix1" "d146cdd5b66ca1d3ed65ce93ca42c6db22defc09"
"tsfix2" "c82d91c6bccf307186332b6c5c6fc0b128b1b2b1"
"task3a" "707442f5de416fdbed4681a33b739f0a787b7834"
"task3b" "431b28e0cfdc9cac6c649193cf602108a8b02997"
"task3c" "0041b013a473e3ae72f50209d8f79db75a72848b"})
(def cli-opts
"Additional command line options."
[["-p" "--patch NAME" "Garage patch to use"
:default "default"
:validate [patches (cli/one-of patches)]]
["-s" "--scenario NAME" "Nemesis scenario to run"
:default "cp"
:validate [scenari (cli/one-of scenari)]]
["-r" "--rate HZ" "Approximate number of requests per second, per thread."
:default 10
:parse-fn read-string
:validate [#(and (number? %) (pos? %)) "Must be a positive number"]]
[nil "--ops-per-key NUM" "Maximum number of operations on any given key."
:default 100
:parse-fn parse-long
:validate [pos? "Must be a positive integer."]]
["-w" "--workload NAME" "Workload of test to run"
:default "reg1"
:validate [workloads (cli/one-of workloads)]]])
(defn garage-test
"Given an options map from the command line runner (e.g. :nodes, :ssh,
:concurrency, ...), constructs a test map."
[opts]
(let [garage-version (get patches (:patch opts))
db (grg/db garage-version)
workload ((get workloads (:workload opts)) opts)
scenario ((get scenari (:scenario opts)) (assoc opts :db db))]
(merge tests/noop-test
opts
{:pure-generators true
:name (str "garage-" (name (:patch opts)) " " (name (:workload opts)) " " (name (:scenario opts)))
:os debian/os
:db db
:client (:client workload)
:generator (gen/phases
(->>
(:generator workload)
(gen/stagger (/ (:rate opts)))
(gen/nemesis (:generator scenario))
(gen/time-limit (:time-limit opts)))
(gen/log "Healing cluster")
(gen/nemesis (:final-generator scenario))
(gen/log "Waiting for recovery")
(gen/sleep 10)
(gen/log "Running final generator")
(gen/clients (:final-generator workload))
(gen/log "Generators all done"))
:nemesis (:nemesis scenario)
:checker (checker/compose
{:perf (checker/perf (:perf scenario))
:workload (:checker workload)})
})))
(defn -main
"Handles command line arguments. Can either run a test, or a web server for
browsing results."
[& args]
(cli/run! (merge (cli/single-test-cmd {:test-fn garage-test
:opt-spec cli-opts})
(cli/serve-cmd))
args))

View file

@ -0,0 +1,152 @@
(ns jepsen.garage.daemon
(:require [clojure.tools.logging :refer :all]
[jepsen [control :as c]
[core :as jepsen]
[db :as db]]
[jepsen.control.util :as cu]))
; CONSTANTS -- HOW GARAGE IS SET UP
(def base-dir "/opt/garage")
(def data-dir (str base-dir "/data"))
(def meta-dir (str base-dir "/meta"))
(def binary (str base-dir "/garage"))
(def logfile (str base-dir "/garage.log"))
(def pidfile (str base-dir "/garage.pid"))
(def admin-token "icanhazadmin")
(def access-key-id "GK8bfb6a51286071c6c9cd8bc3")
(def secret-access-key "b0be95f71c1c6f16858a9edf395078b75c12ecb6b1c03385c4ae92076e4994a3")
(def bucket-name "jepsen")
; THE GARAGE DB
(defn install!
"Download and install Garage"
[node version]
(c/su
(c/trace
(info node "installing garage" version)
(c/exec :mkdir :-p base-dir)
(let [url (str "https://garagehq.deuxfleurs.fr/_releases/" version "/x86_64-unknown-linux-musl/garage")
cache (cu/cached-wget! url)]
(c/exec :cp cache binary))
(c/exec :chmod :+x binary))))
(defn configure!
"Configure Garage"
[node]
(c/su
(c/trace
(cu/write-file!
(str "rpc_secret = \"0fffabe52542c2b89a56b2efb7dfd477e9dafb285c9025cbdf1de7ca21a6b372\"\n"
"rpc_bind_addr = \"0.0.0.0:3901\"\n"
"rpc_public_addr = \"" node ":3901\"\n"
"db_engine = \"lmdb\"\n"
"replication_mode = \"2\"\n"
"data_dir = \"" data-dir "\"\n"
"metadata_dir = \"" meta-dir "\"\n"
"[s3_api]\n"
"s3_region = \"us-east-1\"\n"
"api_bind_addr = \"0.0.0.0:3900\"\n"
"[k2v_api]\n"
"api_bind_addr = \"0.0.0.0:3902\"\n"
"[admin]\n"
"api_bind_addr = \"0.0.0.0:3903\"\n"
"admin_token = \"" admin-token "\"\n"
"trace_sink = \"http://192.168.56.1:4317\"\n")
"/etc/garage.toml"))))
(defn connect-node!
"Connect a Garage node to the rest of the cluster"
[test node]
(c/trace
(let [node-id (c/exec binary :node :id :-q)]
(info node "node id:" node-id)
(c/on-many (:nodes test)
(c/exec binary :node :connect node-id)))))
(defn configure-node!
"Configure a Garage node to be part of a cluster layout"
[test node]
(c/trace
(let [node-id (c/exec binary :node :id :-q)]
(c/on (jepsen/primary test)
(c/exec binary :layout :assign (subs node-id 0 16) :-c :1G :-z :dc1 :-t node)))))
(defn finalize-config!
"Apply the layout and create a key/bucket pair in the cluster"
[node]
(c/trace
(c/exec binary :layout :apply :--version 1)
(info node "garage status:" (c/exec binary :status))
(c/exec binary :key :import access-key-id secret-access-key :--yes)
(c/exec binary :bucket :create bucket-name)
(c/exec binary :bucket :allow :--read :--write bucket-name :--key access-key-id)
(info node "key info: " (c/exec binary :key :info access-key-id))))
(defn db
"Garage DB for a particular version"
[version]
(reify db/DB
(setup! [_ test node]
(install! node version)
(configure! node)
(cu/start-daemon!
{:logfile logfile
:pidfile pidfile
:chdir base-dir
:env {:RUST_LOG "garage=debug,garage_api=trace"}}
binary
:server)
(c/exec :sleep 3)
(jepsen/synchronize test)
(connect-node! test node)
(jepsen/synchronize test)
(configure-node! test node)
(jepsen/synchronize test)
(when (= node (jepsen/primary test))
(finalize-config! node)))
(teardown! [_ test node]
(info node "tearing down garage" version)
(c/su
(cu/stop-daemon! binary pidfile)
(c/exec :rm :-rf logfile)
(c/exec :rm :-rf data-dir)
(c/exec :rm :-rf meta-dir)))
db/Pause
(pause! [_ test node]
(cu/grepkill! :stop binary))
(resume! [_ test node]
(cu/grepkill! :cont binary))
db/Kill
(kill! [_ test node]
(cu/stop-daemon! binary pidfile))
(start! [_ test node]
(cu/start-daemon!
{:logfile logfile
:pidfile pidfile
:chdir base-dir
:env {:RUST_LOG "garage=debug,garage_api=trace"}}
binary
:server))
db/LogFiles
(log-files [_ test node]
[logfile])))
(defn creds
"Obtain Garage credentials for node"
[node]
{:access-key access-key-id
:secret-key secret-access-key
:endpoint (str "http://" node ":3900")
:bucket bucket-name
:client-config {:path-style-access-enabled true}})

View file

@ -0,0 +1,142 @@
(ns jepsen.garage.nemesis
(:require [clojure.tools.logging :refer :all]
[jepsen [control :as c]
[core :as jepsen]
[generator :as gen]
[nemesis :as nemesis]]
[jepsen.nemesis.combined :as combined]
[jepsen.garage.daemon :as grg]
[jepsen.control.util :as cu]))
; ---- reconfiguration nemesis ----
(defn configure-present!
"Configure node to be active in new cluster layout"
[test nodes]
(info "configure-present!" nodes)
(let [node-ids (c/on-many nodes (c/exec grg/binary :node :id :-q))
node-id-strs (map (fn [[_ v]] (subs v 0 16)) node-ids)]
(c/on
(jepsen/primary test)
(apply c/exec (concat [grg/binary :layout :assign :-c :1G] node-id-strs)))))
(defn configure-absent!
"Configure nodes to be active in new cluster layout"
[test nodes]
(info "configure-absent!" nodes)
(let [node-ids (c/on-many nodes (c/exec grg/binary :node :id :-q))
node-id-strs (map (fn [[_ v]] (subs v 0 16)) node-ids)]
(c/on
(jepsen/primary test)
(apply c/exec (concat [grg/binary :layout :assign :-g] node-id-strs)))))
(defn finalize-config!
"Apply the proposed cluster layout"
[test]
(let [layout-show (c/on (jepsen/primary test) (c/exec grg/binary :layout :show))
[_ layout-next-version] (re-find #"apply --version (\d+)\n" layout-show)]
(if layout-next-version
(do
(info "layout show: " layout-show "; next-version: " layout-next-version)
(c/on (jepsen/primary test)
(c/exec grg/binary :layout :apply :--version layout-next-version)))
(info "no layout changes to apply"))))
(defn reconfigure-subset
"Reconfigure cluster with only a subset of nodes"
[cnt]
(reify nemesis/Nemesis
(setup! [this test] this)
(invoke! [this test op] op
(case (:f op)
:start
(let [[keep-nodes remove-nodes]
(->> (:nodes test)
shuffle
(split-at cnt))]
(info "layout split: keep " keep-nodes ", remove " remove-nodes)
(configure-present! test keep-nodes)
(configure-absent! test remove-nodes)
(finalize-config! test)
(assoc op :value keep-nodes))
:stop
(do
(info "layout un-split: all nodes=" (:nodes test))
(configure-present! test (:nodes test))
(finalize-config! test)
(assoc op :value (:nodes test)))))
(teardown! [this test] this)))
; ---- nemesis scenari ----
(defn nemesis-op
"A generator for a single nemesis operation"
[op]
(fn [_ _] {:type :info, :f op}))
(defn reconfiguration-package
"Cluster reconfiguration nemesis package"
[opts]
{:generator (->>
(gen/mix [(nemesis-op :reconfigure-start)
(nemesis-op :reconfigure-stop)])
(gen/stagger (:interval opts 5)))
:final-generator {:type :info, :f :reconfigure-stop}
:nemesis (nemesis/compose
{{:reconfigure-start :start
:reconfigure-stop :stop} (reconfigure-subset 3)})
:perf #{{:name "reconfigure"
:start #{:reconfigure-start}
:stop #{:reconfigur-stop}
:color "#A197E9"}}})
(defn scenario-c
"Clock modifying scenario"
[opts]
(combined/clock-package {:db (:db opts), :interval 1, :faults #{:clock}}))
(defn scenario-cp
"Clock modifying + partition scenario"
[opts]
(combined/compose-packages
[(combined/clock-package {:db (:db opts), :interval 1, :faults #{:clock}})
(combined/partition-package {:db (:db opts), :interval 1, :faults #{:partition}})]))
(defn scenario-r
"Cluster reconfiguration scenario"
[opts]
(reconfiguration-package {:interval 1}))
(defn scenario-pr
"Partition + cluster reconfiguration scenario"
[opts]
(combined/compose-packages
[(combined/partition-package {:db (:db opts), :interval 1, :faults #{:partition}})
(reconfiguration-package {:interval 1})]))
(defn scenario-cpr
"Clock scramble + partition + cluster reconfiguration scenario"
[opts]
(combined/compose-packages
[(combined/clock-package {:db (:db opts), :interval 1, :faults #{:clock}})
(combined/partition-package {:db (:db opts), :interval 1, :faults #{:partition}})
(reconfiguration-package {:interval 1})]))
(defn scenario-cdp
"Clock modifying + db + partition scenario"
[opts]
(combined/compose-packages
[(combined/clock-package {:db (:db opts), :interval 1, :faults #{:clock}})
(combined/db-package {:db (:db opts), :interval 1, :faults #{:db :pause :kill}})
(combined/partition-package {:db (:db opts), :interval 1, :faults #{:partition}})]))
(defn scenario-dpr
"Db + partition + cluster reconfiguration scenario"
[opts]
(combined/compose-packages
[(combined/db-package {:db (:db opts), :interval 1, :faults #{:db :pause :kill}})
(combined/partition-package {:db (:db opts), :interval 1, :faults #{:partition}})
(reconfiguration-package {:interval 1})]))

View file

@ -0,0 +1,143 @@
(ns jepsen.garage.reg
(:require [clojure.tools.logging :refer :all]
[clojure.string :as str]
[clojure.set :as set]
[jepsen [checker :as checker]
[cli :as cli]
[client :as client]
[control :as c]
[db :as db]
[generator :as gen]
[independent :as independent]
[nemesis :as nemesis]
[util :as util]
[tests :as tests]]
[jepsen.checker.timeline :as timeline]
[jepsen.control.util :as cu]
[jepsen.os.debian :as debian]
[jepsen.garage.daemon :as grg]
[jepsen.garage.s3api :as s3]
[knossos.model :as model]
[slingshot.slingshot :refer [try+]]))
(defn op-get [_ _] {:type :invoke, :f :read, :value nil})
(defn op-put [_ _] {:type :invoke, :f :write, :value (str (rand-int 99))})
(defn op-del [_ _] {:type :invoke, :f :write, :value nil})
(defrecord RegClient [creds]
client/Client
(open! [this test node]
(assoc this :creds (grg/creds node)))
(setup! [this test])
(invoke! [this test op]
(try+
(let [[k v] (:value op)]
(case (:f op)
:read
(util/timeout
10000
(assoc op :type :fail, :error ::timeout)
(let [value (s3/get (:creds this) k)]
(assoc op :type :ok, :value (independent/tuple k value))))
:write
(util/timeout
10000
(assoc op :type :info, :error ::timeout)
(do
(s3/put (:creds this) k v)
(assoc op :type :ok)))))
(catch (re-find #"Unavailable" (.getMessage %)) ex
(assoc op :type :info, :error ::unavailable))
(catch (re-find #"Broken pipe" (.getMessage %)) ex
(assoc op :type :info, :error ::broken-pipe))
(catch (re-find #"Connection refused" (.getMessage %)) ex
(assoc op :type :info, :error ::connection-refused))))
(teardown! [this test])
(close! [this test]))
(defn reg-read-after-write
"Read-after-Write checker for register operations"
[]
(reify checker/Checker
(check [this test history opts]
(let [init {:put-values {-1 nil}
:put-done #{-1}
:put-in-progress {}
:read-can-contain {}
:bad-reads #{}}
final (reduce
(fn [state op]
(let [current-values (set/union
(set (map (fn [idx] (get (:put-values state) idx)) (:put-done state)))
(set (map (fn [[_ [idx _]]] (get (:put-values state) idx)) (:put-in-progress state))))
read-can-contain (reduce
(fn [rcc [idx v]] (assoc rcc idx (set/union current-values v)))
{} (:read-can-contain state))]
(info "--------")
(info "state: " state)
(info "current-values: " current-values)
(info "read-can-contain: " read-can-contain)
(info "op: " op)
(case [(:type op) (:f op)]
([:invoke :write])
(assoc state
:read-can-contain read-can-contain
:put-values (assoc (:put-values state) (:index op) (:value op))
:put-in-progress (assoc (:put-in-progress state) (:process op) [(:index op) (:put-done state)]))
([:ok :write])
(let [[index overwrites] (get (:put-in-progress state) (:process op))]
(assoc state
:read-can-contain read-can-contain
:put-in-progress (dissoc (:put-in-progress state) (:process op))
:put-done
(conj
(set/difference (:put-done state) overwrites)
index)))
([:invoke :read])
(assoc state
:read-can-contain (assoc read-can-contain (:process op) current-values))
([:ok :read])
(let [this-read-can-contain (get read-can-contain (:process op))
bad-reads (if (contains? this-read-can-contain (:value op))
(:bad-reads state)
(conj (:bad-reads state) [(:process op) (:index op) (:value op) this-read-can-contain]))]
(info "this-read-can-contain: " this-read-can-contain)
(assoc state
:read-can-contain (dissoc read-can-contain (:process op))
:bad-reads bad-reads))
state)))
init history)
valid? (empty? (:bad-reads final))]
(assoc final :valid? valid?)))))
(defn workload-common
"Common parts of workload"
[opts]
{:client (RegClient. nil)
:generator (independent/concurrent-generator
10
(range)
(fn [k]
(->>
(gen/mix [op-get op-put op-del])
(gen/limit (:ops-per-key opts)))))})
(defn workload1
"Tests linearizable reads and writes"
[opts]
(assoc (workload-common opts)
:checker (independent/checker
(checker/compose
{:linear (checker/linearizable
{:model (model/register)
:algorithm :linear})
:timeline (timeline/html)}))))
(defn workload2
"Tests CRDT reads and writes"
[opts]
(assoc (workload-common opts)
:checker (independent/checker
(checker/compose
{:reg-read-after-write (reg-read-after-write)
:timeline (timeline/html)}))))

View file

@ -0,0 +1,48 @@
(ns jepsen.garage.s3api
(:require [clojure.tools.logging :refer :all]
[jepsen [control :as c]]
[amazonica.aws.s3 :as s3]
[slingshot.slingshot :refer [try+]]))
; GARAGE S3 HELPER FUNCTIONS
(defn get
"Helper for GetObject"
[creds k]
(try+
(-> (s3/get-object creds (:bucket creds) k)
:input-stream
slurp)
(catch (re-find #"Key not found" (.getMessage %)) ex
nil)))
(defn put
"Helper for PutObject or DeleteObject (is a delete if value is nil)"
[creds k v]
(if (= v nil)
(s3/delete-object creds
:bucket-name (:bucket creds)
:key k)
(let [some-bytes (.getBytes v "UTF-8")
bytes-stream (java.io.ByteArrayInputStream. some-bytes)]
(s3/put-object creds
:bucket-name (:bucket creds)
:key k
:input-stream bytes-stream
:metadata {:content-length (count some-bytes)}))))
(defn list-inner [creds prefix ct accum]
(let [list-result (s3/list-objects-v2 creds
{:bucket-name (:bucket creds)
:prefix prefix
:continuation-token ct})
new-object-summaries (:object-summaries list-result)
new-objects (map (fn [d] (:key d)) new-object-summaries)
objects (concat new-objects accum)]
(if (:truncated? list-result)
(list-inner creds prefix (:next-continuation-token list-result) objects)
objects)))
(defn list
"Helper for ListObjects -- just lists everything in the bucket"
[creds prefix]
(list-inner creds prefix nil []))

View file

@ -0,0 +1,135 @@
(ns jepsen.garage.set
(:require [clojure.tools.logging :refer :all]
[clojure.string :as str]
[clojure.set :as set]
[jepsen [checker :as checker]
[cli :as cli]
[client :as client]
[control :as c]
[checker :as checker]
[db :as db]
[generator :as gen]
[independent :as independent]
[nemesis :as nemesis]
[util :as util]
[tests :as tests]]
[jepsen.checker.timeline :as timeline]
[jepsen.control.util :as cu]
[jepsen.os.debian :as debian]
[jepsen.garage.daemon :as grg]
[jepsen.garage.s3api :as s3]
[knossos.model :as model]
[slingshot.slingshot :refer [try+]]))
(defn op-add-rand100 [_ _] {:type :invoke, :f :add, :value (rand-int 100)})
(defn op-read [_ _] {:type :invoke, :f :read, :value nil})
(defrecord SetClient [creds]
client/Client
(open! [this test node]
(assoc this :creds (grg/creds node)))
(setup! [this test])
(invoke! [this test op]
(try+
(let [[k v] (:value op)
prefix (str "set" k "/")]
(case (:f op)
:add
(util/timeout
10000
(assoc op :type :info, :error ::timeout)
(do
(s3/put (:creds this) (str prefix v) "present")
(assoc op :type :ok)))
:read
(util/timeout
10000
(assoc op :type :fail, :error ::timeout)
(do
(let [items (s3/list (:creds this) prefix)]
(let [items-stripped (map (fn [o]
(assert (str/starts-with? o prefix))
(str/replace-first o prefix "")) items)
items-set (set (map parse-long items-stripped))]
(assoc op :type :ok, :value (independent/tuple k items-set))))))))
(catch (re-find #"Unavailable" (.getMessage %)) ex
(assoc op :type :info, :error ::unavailable))
(catch (re-find #"Broken pipe" (.getMessage %)) ex
(assoc op :type :info, :error ::broken-pipe))
(catch (re-find #"Connection refused" (.getMessage %)) ex
(assoc op :type :info, :error ::connection-refused))))
(teardown! [this test])
(close! [this test]))
(defn set-read-after-write
"Read-after-Write checker for set operations"
[]
(reify checker/Checker
(check [this test history opts]
(let [init {:add-started #{}
:add-done #{}
:read-must-contain {}
:missed #{}
:unexpected #{}}
final (reduce
(fn [state op]
(case [(:type op) (:f op)]
([:invoke :add])
(assoc state :add-started (conj (:add-started state) (:value op)))
([:ok :add])
(assoc state :add-done (conj (:add-done state) (:value op)))
([:invoke :read])
(assoc-in state [:read-must-contain (:process op)] (:add-done state))
([:ok :read])
(let [read-must-contain (get (:read-must-contain state) (:process op))
new-missed (set/difference read-must-contain (:value op))
new-unexpected (set/difference (:value op) (:add-started state))]
(assoc state
:read-must-contain (dissoc (:read-must-contain state) (:process op))
:missed (set/union (:missed state) new-missed),
:unexpected (set/union (:unexpected state) new-unexpected)))
state))
init history)
valid? (and (empty? (:missed final)) (empty? (:unexpected final)))]
(assoc final :valid? valid?)))))
(defn workload1
"Tests insertions and deletions"
[opts]
{:client (SetClient. nil)
:checker (independent/checker
(checker/compose
{:set (checker/set)
:timeline (timeline/html)}))
:generator (independent/concurrent-generator
10
(range 100)
(fn [k]
(->> (range)
(map (fn [x] {:type :invoke, :f :add, :value x}))
(gen/limit (:ops-per-key opts)))))
:final-generator (independent/concurrent-generator
10
(range 100)
(fn [k]
(gen/phases
(gen/once op-read)
(gen/sleep 5))))})
(defn workload2
"Tests insertions and deletions"
[opts]
{:client (SetClient. nil)
:checker (independent/checker
(checker/compose
{:set-read-after-write (set-read-after-write)
; :set-full (checker/set-full {:linearizable? false})
:timeline (timeline/html)}))
:generator (independent/concurrent-generator
10
(range)
(fn [k]
(->> (gen/mix [op-add-rand100 op-read])
(gen/limit (:ops-per-key opts)))))})

View file

@ -0,0 +1,7 @@
(ns jepsen.garage-test
(:require [clojure.test :refer :all]
[jepsen.garage :refer :all]))
(deftest a-test
(testing "FIXME, I fail."
(is (= 0 1))))

View file

@ -182,7 +182,7 @@ impl AdminApiServer {
), ),
}; };
let status_str = format!( let status_str = format!(
"{}\nConsult the full health check API endpoint at /v0/health for more details\n", "{}\nConsult the full health check API endpoint at /v1/health for more details\n",
status_str status_str
); );

View file

@ -344,7 +344,7 @@ impl ApiHandler for S3ApiServer {
bucket_id, bucket_id,
key, key,
upload_id, upload_id,
part_number_marker: part_number_marker.map(|p| p.clamp(1, 10000)), part_number_marker: part_number_marker.map(|p| p.min(10000)),
max_parts: max_parts.unwrap_or(1000).clamp(1, 1000), max_parts: max_parts.unwrap_or(1000).clamp(1, 1000),
}, },
) )

View file

@ -15,6 +15,7 @@ use serde::Deserialize;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use crate::s3::cors::*;
use crate::s3::error::*; use crate::s3::error::*;
use crate::s3::put::{get_headers, save_stream}; use crate::s3::put::{get_headers, save_stream};
use crate::s3::xml as s3_xml; use crate::s3::xml as s3_xml;
@ -242,7 +243,7 @@ pub async fn handle_post_object(
let etag = format!("\"{}\"", md5); let etag = format!("\"{}\"", md5);
let resp = if let Some(mut target) = params let mut resp = if let Some(mut target) = params
.get("success_action_redirect") .get("success_action_redirect")
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
.and_then(|u| url::Url::parse(u).ok()) .and_then(|u| url::Url::parse(u).ok())
@ -262,8 +263,7 @@ pub async fn handle_post_object(
} else { } else {
let path = head let path = head
.uri .uri
.into_parts() .path_and_query()
.path_and_query
.map(|paq| paq.path().to_string()) .map(|paq| paq.path().to_string())
.unwrap_or_else(|| "/".to_string()); .unwrap_or_else(|| "/".to_string());
let authority = head let authority = head
@ -308,6 +308,13 @@ pub async fn handle_post_object(
} }
}; };
let matching_cors_rule =
find_matching_cors_rule(&bucket, &Request::from_parts(head, Body::empty()))?;
if let Some(rule) = matching_cors_rule {
add_cors_headers(&mut resp, rule)
.ok_or_internal_error("Invalid bucket CORS configuration")?;
}
Ok(resp) Ok(resp)
} }

View file

@ -67,6 +67,7 @@ chrono = "0.4"
http = "0.2" http = "0.2"
hmac = "0.12" hmac = "0.12"
hyper = { version = "0.14", features = ["client", "http1", "runtime"] } hyper = { version = "0.14", features = ["client", "http1", "runtime"] }
mktemp = "0.5"
sha2 = "0.10" sha2 = "0.10"
static_init = "1.0" static_init = "1.0"

View file

@ -7,6 +7,7 @@ extern crate tracing;
mod admin; mod admin;
mod cli; mod cli;
mod repair; mod repair;
mod secrets;
mod server; mod server;
#[cfg(feature = "telemetry-otlp")] #[cfg(feature = "telemetry-otlp")]
mod tracing_setup; mod tracing_setup;
@ -28,7 +29,6 @@ use structopt::StructOpt;
use netapp::util::parse_and_resolve_peer_addr; use netapp::util::parse_and_resolve_peer_addr;
use netapp::NetworkKey; use netapp::NetworkKey;
use garage_util::config::Config;
use garage_util::error::*; use garage_util::error::*;
use garage_rpc::system::*; use garage_rpc::system::*;
@ -38,6 +38,7 @@ use garage_model::helper::error::Error as HelperError;
use admin::*; use admin::*;
use cli::*; use cli::*;
use secrets::Secrets;
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
#[structopt( #[structopt(
@ -66,24 +67,6 @@ struct Opt {
cmd: Command, cmd: Command,
} }
#[derive(StructOpt, Debug)]
pub struct Secrets {
/// RPC secret network key, used to replace rpc_secret in config.toml when running the
/// daemon or doing admin operations
#[structopt(short = "s", long = "rpc-secret", env = "GARAGE_RPC_SECRET")]
pub rpc_secret: Option<String>,
/// Metrics API authentication token, replaces admin.metrics_token in config.toml when
/// running the Garage daemon
#[structopt(long = "admin-token", env = "GARAGE_ADMIN_TOKEN")]
pub admin_token: Option<String>,
/// Metrics API authentication token, replaces admin.metrics_token in config.toml when
/// running the Garage daemon
#[structopt(long = "metrics-token", env = "GARAGE_METRICS_TOKEN")]
pub metrics_token: Option<String>,
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Initialize version and features info // Initialize version and features info
@ -261,16 +244,3 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
Ok(x) => Ok(x), Ok(x) => Ok(x),
} }
} }
fn fill_secrets(mut config: Config, secrets: Secrets) -> Config {
if secrets.rpc_secret.is_some() {
config.rpc_secret = secrets.rpc_secret;
}
if secrets.admin_token.is_some() {
config.admin.admin_token = secrets.admin_token;
}
if secrets.metrics_token.is_some() {
config.admin.metrics_token = secrets.metrics_token;
}
config
}

View file

@ -6,7 +6,7 @@ use garage_util::error::*;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use crate::cli::structs::*; use crate::cli::structs::*;
use crate::{fill_secrets, Secrets}; use crate::secrets::{fill_secrets, Secrets};
pub async fn offline_repair( pub async fn offline_repair(
config_file: PathBuf, config_file: PathBuf,
@ -20,7 +20,7 @@ pub async fn offline_repair(
} }
info!("Loading configuration..."); info!("Loading configuration...");
let config = fill_secrets(read_config(config_file)?, secrets); let config = fill_secrets(read_config(config_file)?, secrets)?;
info!("Initializing Garage main data store..."); info!("Initializing Garage main data store...");
let garage = Garage::new(config)?; let garage = Garage::new(config)?;

318
src/garage/secrets.rs Normal file
View file

@ -0,0 +1,318 @@
use structopt::StructOpt;
use garage_util::config::Config;
use garage_util::error::Error;
/// Structure for secret values or paths that are passed as CLI arguments or environment
/// variables, instead of in the config file.
#[derive(StructOpt, Debug, Default, Clone)]
pub struct Secrets {
/// Skip permission check on files containing secrets
#[cfg(unix)]
#[structopt(
long = "allow-world-readable-secrets",
env = "GARAGE_ALLOW_WORLD_READABLE_SECRETS"
)]
pub allow_world_readable_secrets: Option<bool>,
/// RPC secret network key, used to replace rpc_secret in config.toml when running the
/// daemon or doing admin operations
#[structopt(short = "s", long = "rpc-secret", env = "GARAGE_RPC_SECRET")]
pub rpc_secret: Option<String>,
/// RPC secret network key, used to replace rpc_secret in config.toml and rpc-secret
/// when running the daemon or doing admin operations
#[structopt(long = "rpc-secret-file", env = "GARAGE_RPC_SECRET_FILE")]
pub rpc_secret_file: Option<String>,
/// Admin API authentication token, replaces admin.admin_token in config.toml when
/// running the Garage daemon
#[structopt(long = "admin-token", env = "GARAGE_ADMIN_TOKEN")]
pub admin_token: Option<String>,
/// Admin API authentication token file path, replaces admin.admin_token in config.toml
/// and admin-token when running the Garage daemon
#[structopt(long = "admin-token-file", env = "GARAGE_ADMIN_TOKEN_FILE")]
pub admin_token_file: Option<String>,
/// Metrics API authentication token, replaces admin.metrics_token in config.toml when
/// running the Garage daemon
#[structopt(long = "metrics-token", env = "GARAGE_METRICS_TOKEN")]
pub metrics_token: Option<String>,
/// Metrics API authentication token file path, replaces admin.metrics_token in config.toml
/// and metrics-token when running the Garage daemon
#[structopt(long = "metrics-token-file", env = "GARAGE_METRICS_TOKEN_FILE")]
pub metrics_token_file: Option<String>,
}
/// Single function to fill all secrets in the Config struct from their correct source (value
/// from config or CLI param or env variable or read from a file specified in config or CLI
/// param or env variable)
pub fn fill_secrets(mut config: Config, secrets: Secrets) -> Result<Config, Error> {
let allow_world_readable = secrets
.allow_world_readable_secrets
.unwrap_or(config.allow_world_readable_secrets);
fill_secret(
&mut config.rpc_secret,
&config.rpc_secret_file,
&secrets.rpc_secret,
&secrets.rpc_secret_file,
"rpc_secret",
allow_world_readable,
)?;
fill_secret(
&mut config.admin.admin_token,
&config.admin.admin_token_file,
&secrets.admin_token,
&secrets.admin_token_file,
"admin.admin_token",
allow_world_readable,
)?;
fill_secret(
&mut config.admin.metrics_token,
&config.admin.metrics_token_file,
&secrets.metrics_token,
&secrets.metrics_token_file,
"admin.metrics_token",
allow_world_readable,
)?;
Ok(config)
}
fn fill_secret(
config_secret: &mut Option<String>,
config_secret_file: &Option<String>,
cli_secret: &Option<String>,
cli_secret_file: &Option<String>,
name: &'static str,
allow_world_readable: bool,
) -> Result<(), Error> {
let cli_value = match (&cli_secret, &cli_secret_file) {
(Some(_), Some(_)) => {
return Err(format!("only one of `{}` and `{}_file` can be set", name, name).into());
}
(Some(secret), None) => Some(secret.to_string()),
(None, Some(file)) => Some(read_secret_file(file, allow_world_readable)?),
(None, None) => None,
};
if let Some(val) = cli_value {
if config_secret.is_some() || config_secret_file.is_some() {
debug!("Overriding secret `{}` using value specified using CLI argument or environnement variable.", name);
}
*config_secret = Some(val);
} else if let Some(file_path) = &config_secret_file {
if config_secret.is_some() {
return Err(format!("only one of `{}` and `{}_file` can be set", name, name).into());
}
*config_secret = Some(read_secret_file(file_path, allow_world_readable)?);
}
Ok(())
}
fn read_secret_file(file_path: &String, allow_world_readable: bool) -> Result<String, Error> {
if !allow_world_readable {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let metadata = std::fs::metadata(file_path)?;
if metadata.mode() & 0o077 != 0 {
return Err(format!("File {} is world-readable! (mode: 0{:o}, expected 0600)\nRefusing to start until this is fixed, or environment variable GARAGE_ALLOW_WORLD_READABLE_SECRETS is set to true.", file_path, metadata.mode()).into());
}
}
}
let secret_buf = std::fs::read_to_string(file_path)?;
// trim_end: allows for use case such as `echo "$(openssl rand -hex 32)" > somefile`.
// also editors sometimes add a trailing newline
Ok(String::from(secret_buf.trim_end()))
}
#[cfg(test)]
mod tests {
use std::fs::File;
use std::io::Write;
use garage_util::config::read_config;
use garage_util::error::Error;
use super::*;
#[test]
fn test_rpc_secret_file_works() -> Result<(), Error> {
let path_secret = mktemp::Temp::new_file()?;
let mut file_secret = File::create(path_secret.as_path())?;
writeln!(file_secret, "foo")?;
drop(file_secret);
let path_config = mktemp::Temp::new_file()?;
let mut file_config = File::create(path_config.as_path())?;
let path_secret_path = path_secret.as_path();
writeln!(
file_config,
r#"
metadata_dir = "/tmp/garage/meta"
data_dir = "/tmp/garage/data"
replication_mode = "3"
rpc_bind_addr = "[::]:3901"
rpc_secret_file = "{}"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
"#,
path_secret_path.display()
)?;
drop(file_config);
// Second configuration file, same as previous one
// except it allows world-readable secrets.
let path_config_allow_world_readable = mktemp::Temp::new_file()?;
let mut file_config_allow_world_readable =
File::create(path_config_allow_world_readable.as_path())?;
writeln!(
file_config_allow_world_readable,
r#"
metadata_dir = "/tmp/garage/meta"
data_dir = "/tmp/garage/data"
replication_mode = "3"
rpc_bind_addr = "[::]:3901"
rpc_secret_file = "{}"
allow_world_readable_secrets = true
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
"#,
path_secret_path.display()
)?;
drop(file_config_allow_world_readable);
let config = read_config(path_config.to_path_buf())?;
let config = fill_secrets(config, Secrets::default())?;
assert_eq!("foo", config.rpc_secret.unwrap());
// ---- Check non world-readable secrets config ----
#[cfg(unix)]
{
let secrets_allow_world_readable = Secrets {
allow_world_readable_secrets: Some(true),
..Default::default()
};
let secrets_no_allow_world_readable = Secrets {
allow_world_readable_secrets: Some(false),
..Default::default()
};
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(&path_secret_path)?;
let mut perm = metadata.permissions();
perm.set_mode(0o660);
std::fs::set_permissions(&path_secret_path, perm)?;
// Config file that just specifies the path
let config = read_config(path_config.to_path_buf())?;
assert!(fill_secrets(config, Secrets::default()).is_err());
let config = read_config(path_config.to_path_buf())?;
assert!(fill_secrets(config, secrets_allow_world_readable.clone()).is_ok());
let config = read_config(path_config.to_path_buf())?;
assert!(fill_secrets(config, secrets_no_allow_world_readable.clone()).is_err());
// Config file that also specifies to allow world_readable_secrets
let config = read_config(path_config_allow_world_readable.to_path_buf())?;
assert!(fill_secrets(config, Secrets::default()).is_ok());
let config = read_config(path_config_allow_world_readable.to_path_buf())?;
assert!(fill_secrets(config, secrets_allow_world_readable).is_ok());
let config = read_config(path_config_allow_world_readable.to_path_buf())?;
assert!(fill_secrets(config, secrets_no_allow_world_readable).is_err());
}
// ---- Check alternative secrets specified on CLI ----
let path_secret2 = mktemp::Temp::new_file()?;
let mut file_secret2 = File::create(path_secret2.as_path())?;
writeln!(file_secret2, "bar")?;
drop(file_secret2);
let config = read_config(path_config.to_path_buf())?;
let config = fill_secrets(
config,
Secrets {
rpc_secret: Some("baz".into()),
..Default::default()
},
)?;
assert_eq!(config.rpc_secret.as_deref(), Some("baz"));
let config = read_config(path_config.to_path_buf())?;
let config = fill_secrets(
config,
Secrets {
rpc_secret_file: Some(path_secret2.display().to_string()),
..Default::default()
},
)?;
assert_eq!(config.rpc_secret.as_deref(), Some("bar"));
let config = read_config(path_config.to_path_buf())?;
assert!(fill_secrets(
config,
Secrets {
rpc_secret: Some("baz".into()),
rpc_secret_file: Some(path_secret2.display().to_string()),
..Default::default()
}
)
.is_err());
drop(path_secret);
drop(path_secret2);
drop(path_config);
drop(path_config_allow_world_readable);
Ok(())
}
#[test]
fn test_rcp_secret_and_rpc_secret_file_cannot_be_set_both() -> Result<(), Error> {
let path_config = mktemp::Temp::new_file()?;
let mut file_config = File::create(path_config.as_path())?;
writeln!(
file_config,
r#"
metadata_dir = "/tmp/garage/meta"
data_dir = "/tmp/garage/data"
replication_mode = "3"
rpc_bind_addr = "[::]:3901"
rpc_secret= "dummy"
rpc_secret_file = "dummy"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
"#
)?;
let config = read_config(path_config.to_path_buf())?;
assert_eq!(
"only one of `rpc_secret` and `rpc_secret_file` can be set",
fill_secrets(config, Secrets::default())
.unwrap_err()
.to_string()
);
drop(path_config);
drop(file_config);
Ok(())
}
}

View file

@ -15,9 +15,9 @@ use garage_web::WebServer;
use garage_api::k2v::api_server::K2VApiServer; use garage_api::k2v::api_server::K2VApiServer;
use crate::admin::*; use crate::admin::*;
use crate::secrets::{fill_secrets, Secrets};
#[cfg(feature = "telemetry-otlp")] #[cfg(feature = "telemetry-otlp")]
use crate::tracing_setup::*; use crate::tracing_setup::*;
use crate::{fill_secrets, Secrets};
async fn wait_from(mut chan: watch::Receiver<bool>) { async fn wait_from(mut chan: watch::Receiver<bool>) {
while !*chan.borrow() { while !*chan.borrow() {
@ -29,12 +29,19 @@ async fn wait_from(mut chan: watch::Receiver<bool>) {
pub async fn run_server(config_file: PathBuf, secrets: Secrets) -> Result<(), Error> { pub async fn run_server(config_file: PathBuf, secrets: Secrets) -> Result<(), Error> {
info!("Loading configuration..."); info!("Loading configuration...");
let config = fill_secrets(read_config(config_file)?, secrets); let config = fill_secrets(read_config(config_file)?, secrets)?;
// ---- Initialize Garage internals ---- // ---- Initialize Garage internals ----
#[cfg(feature = "metrics")] #[cfg(feature = "metrics")]
let metrics_exporter = opentelemetry_prometheus::exporter().init(); let metrics_exporter = opentelemetry_prometheus::exporter()
.with_default_summary_quantiles(vec![0.25, 0.5, 0.75, 0.9, 0.95, 0.99])
.with_default_histogram_boundaries(vec![
0.001, 0.0015, 0.002, 0.003, 0.005, 0.007, 0.01, 0.015, 0.02, 0.03, 0.05, 0.07, 0.1,
0.15, 0.2, 0.3, 0.5, 0.7, 1., 1.5, 2., 3., 5., 7., 10., 15., 20., 30., 40., 50., 60.,
70., 100.,
])
.init();
info!("Initializing Garage main data store..."); info!("Initializing Garage main data store...");
let garage = Garage::new(config.clone())?; let garage = Garage::new(config.clone())?;

View file

@ -1,6 +1,5 @@
//! Contains type and functions related to Garage configuration file //! Contains type and functions related to Garage configuration file
use std::convert::TryFrom; use std::convert::TryFrom;
use std::io::Read;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
@ -45,11 +44,15 @@ pub struct Config {
)] )]
pub compression_level: Option<i32>, pub compression_level: Option<i32>,
/// Skip the permission check of secret files. Useful when
/// POSIX ACLs (or more complex chmods) are used.
#[serde(default)]
pub allow_world_readable_secrets: bool,
/// RPC secret key: 32 bytes hex encoded /// RPC secret key: 32 bytes hex encoded
pub rpc_secret: Option<String>, pub rpc_secret: Option<String>,
/// Optional file where RPC secret key is read from /// Optional file where RPC secret key is read from
pub rpc_secret_file: Option<String>, pub rpc_secret_file: Option<String>,
/// Address to bind for RPC /// Address to bind for RPC
pub rpc_bind_addr: SocketAddr, pub rpc_bind_addr: SocketAddr,
/// Public IP address of this node /// Public IP address of this node
@ -221,6 +224,13 @@ pub struct KubernetesDiscoveryConfig {
pub skip_crd: bool, pub skip_crd: bool,
} }
/// Read and parse configuration
pub fn read_config(config_file: PathBuf) -> Result<Config, Error> {
let config = std::fs::read_to_string(config_file)?;
Ok(toml::from_str(&config)?)
}
fn default_db_engine() -> String { fn default_db_engine() -> String {
"lmdb".into() "lmdb".into()
} }
@ -235,68 +245,6 @@ fn default_block_size() -> usize {
1048576 1048576
} }
/// Read and parse configuration
pub fn read_config(config_file: PathBuf) -> Result<Config, Error> {
let mut file = std::fs::OpenOptions::new()
.read(true)
.open(config_file.as_path())?;
let mut config = String::new();
file.read_to_string(&mut config)?;
let mut parsed_config: Config = toml::from_str(&config)?;
secret_from_file(
&mut parsed_config.rpc_secret,
&parsed_config.rpc_secret_file,
"rpc_secret",
)?;
secret_from_file(
&mut parsed_config.admin.metrics_token,
&parsed_config.admin.metrics_token_file,
"admin.metrics_token",
)?;
secret_from_file(
&mut parsed_config.admin.admin_token,
&parsed_config.admin.admin_token_file,
"admin.admin_token",
)?;
Ok(parsed_config)
}
fn secret_from_file(
secret: &mut Option<String>,
secret_file: &Option<String>,
name: &'static str,
) -> Result<(), Error> {
match (&secret, &secret_file) {
(_, None) => {
// no-op
}
(Some(_), Some(_)) => {
return Err(format!("only one of `{}` and `{}_file` can be set", name, name).into());
}
(None, Some(file_path)) => {
#[cfg(unix)]
if std::env::var("GARAGE_ALLOW_WORLD_READABLE_SECRETS").as_deref() != Ok("true") {
use std::os::unix::fs::MetadataExt;
let metadata = std::fs::metadata(file_path)?;
if metadata.mode() & 0o077 != 0 {
return Err(format!("File {} is world-readable! (mode: 0{:o}, expected 0600)\nRefusing to start until this is fixed, or environment variable GARAGE_ALLOW_WORLD_READABLE_SECRETS is set to true.", file_path, metadata.mode()).into());
}
}
let mut file = std::fs::OpenOptions::new().read(true).open(file_path)?;
let mut secret_buf = String::new();
file.read_to_string(&mut secret_buf)?;
// trim_end: allows for use case such as `echo "$(openssl rand -hex 32)" > somefile`.
// also editors sometimes add a trailing newline
*secret = Some(String::from(secret_buf.trim_end()));
}
}
Ok(())
}
fn default_compression() -> Option<i32> { fn default_compression() -> Option<i32> {
Some(1) Some(1)
} }
@ -425,83 +373,4 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn test_rpc_secret_file_works() -> Result<(), Error> {
let path_secret = mktemp::Temp::new_file()?;
let mut file_secret = File::create(path_secret.as_path())?;
writeln!(file_secret, "foo")?;
drop(file_secret);
let path_config = mktemp::Temp::new_file()?;
let mut file_config = File::create(path_config.as_path())?;
let path_secret_path = path_secret.as_path();
writeln!(
file_config,
r#"
metadata_dir = "/tmp/garage/meta"
data_dir = "/tmp/garage/data"
replication_mode = "3"
rpc_bind_addr = "[::]:3901"
rpc_secret_file = "{}"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
"#,
path_secret_path.display()
)?;
let config = super::read_config(path_config.to_path_buf())?;
assert_eq!("foo", config.rpc_secret.unwrap());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(&path_secret_path)?;
let mut perm = metadata.permissions();
perm.set_mode(0o660);
std::fs::set_permissions(&path_secret_path, perm)?;
std::env::set_var("GARAGE_ALLOW_WORLD_READABLE_SECRETS", "false");
assert!(super::read_config(path_config.to_path_buf()).is_err());
std::env::set_var("GARAGE_ALLOW_WORLD_READABLE_SECRETS", "true");
assert!(super::read_config(path_config.to_path_buf()).is_ok());
}
drop(path_config);
drop(path_secret);
drop(file_config);
Ok(())
}
#[test]
fn test_rcp_secret_and_rpc_secret_file_cannot_be_set_both() -> Result<(), Error> {
let path_config = mktemp::Temp::new_file()?;
let mut file_config = File::create(path_config.as_path())?;
writeln!(
file_config,
r#"
metadata_dir = "/tmp/garage/meta"
data_dir = "/tmp/garage/data"
replication_mode = "3"
rpc_bind_addr = "[::]:3901"
rpc_secret= "dummy"
rpc_secret_file = "dummy"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
"#
)?;
assert_eq!(
"only one of `rpc_secret` and `rpc_secret_file` can be set",
super::read_config(path_config.to_path_buf())
.unwrap_err()
.to_string()
);
drop(path_config);
drop(file_config);
Ok(())
}
} }