Compare commits

...

165 Commits

Author SHA1 Message Date
Alex 5254750658 Merge pull request 'Add TLS support for Consul discovery + refactoring' (#405) from consul-tls into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/405
2022-10-18 20:20:55 +00:00
Alex Auvolat 57b5c2c754 Change reqwest rustls features 2022-10-18 22:11:27 +02:00
Alex Auvolat 8bc5caf7aa Fix issue with 'http(s)://' prefix 2022-10-18 21:17:11 +02:00
Alex Auvolat 2da8786f54 move things around 2022-10-18 19:13:52 +02:00
Alex Auvolat 5d8d393054 Load TLS certificates only once 2022-10-18 19:11:16 +02:00
Alex Auvolat 002b9fc50c Add TLS support for Consul discovery + refactoring 2022-10-18 18:38:20 +02:00
Alex 5670599372 Merge pull request 'Use status code 204 No Content for empty responses' (#403) from tobikris/garage:http-no-content into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/403
2022-10-18 14:20:44 +00:00
Alex 7bc9fd34b2 Merge pull request 'upgrade Nix toolchain' (#400) from upgrade-toolchain into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/400
2022-10-18 14:16:52 +00:00
Alex Auvolat a54a63c491 Add function to upload a build and its dependencies to the cache
to faster bootstrap new runner nodes
2022-10-18 14:19:19 +02:00
Tobias Krischer f1c96d108c update k2v docs for status 204 changes 2022-10-18 13:50:56 +02:00
Alex Auvolat 8fc93abc79 Some things are now in result-bin 2022-10-18 13:39:21 +02:00
Alex Auvolat 667ca9d3e3 Cleanup nix scripts 2022-10-18 12:48:31 +02:00
Alex Auvolat 6a5eba0b72 Add garage_db test to CI 2022-10-18 12:33:35 +02:00
Alex Auvolat 00cf076412 Fix cargo2nix feature discovery 2022-10-18 12:15:45 +02:00
Alex Auvolat 7c0c229934 move refresh_toolchain 2022-10-18 12:15:31 +02:00
Tobias Krischer 7865003323 Use status code 204 No Content for empty responses 2022-10-17 10:55:26 +02:00
Quentin 4582a8f34a Merge pull request 'Update 'doc/book/reference-manual/features.md'' (#402) from borgified/garage:borgified-patch-1 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/402
2022-10-16 07:41:32 +00:00
borgified 8e442001b9 Update 'doc/book/reference-manual/features.md'
typo
2022-10-16 07:13:21 +00:00
Alex Auvolat c050a59fd0 Fix conditional testing in garage_db 2022-10-14 18:27:18 +02:00
Alex Auvolat fcaee3bea0 definitively expunge openssl from dependencies everywhere 2022-10-14 18:10:36 +02:00
Alex Auvolat e89e047c5a Fix i386 build with custom toolchain (armv6 unknown state) 2022-10-14 18:10:24 +02:00
Alex Auvolat 8d04ae7014 cargo2nix unstable (patched), rust 1.63.0, nixpkgs 22.05 (32-bit builds are broken) 2022-10-14 14:30:48 +02:00
Alex a096ced355 Merge pull request 'Fix instant substractions that might have panicked' (#398) from fix-time into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/398
2022-10-02 16:41:06 +02:00
Alex e21b672c96 Merge pull request 'Add helm chart' (#331) from chemicstry/garage:helm_chart into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/331
Reviewed-by: maximilien <me@mricher.fr>
2022-10-02 16:40:54 +02:00
Maximilien R db0c8b3980 Updates values.yml with some opinionated and untested defaults 2022-09-30 18:46:57 +02:00
Maximilien R 6dba7dadf4 Add missing ClusterRole and bindings for CRDs 2022-09-30 18:46:57 +02:00
chemicstry d2c937a931 Fix typo 2022-09-30 18:46:57 +02:00
chemicstry 744c3b4d94 Update docs 2022-09-30 18:46:57 +02:00
chemicstry b71fa2ddf4 Generate random RPC secret if not provided 2022-09-30 18:46:57 +02:00
chemicstry 37a73d7d37 Move documentation to book 2022-09-30 18:46:57 +02:00
chemicstry d0f08c254e Add secret to overrides 2022-09-30 18:46:57 +02:00
chemicstry fa52558ca1 Add configuration instructions to README 2022-09-30 18:46:57 +02:00
chemicstry 131cc2532b Cleanup values.yaml 2022-09-30 18:46:57 +02:00
chemicstry a93dcce841 Add helm chart 2022-09-30 18:46:57 +02:00
Alex b17d59cfab Merge pull request 'Document db_engine' (#399) from doc-0.8 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/399
2022-09-29 17:29:44 +02:00
Alex Auvolat ad917ffd3f Fix instant substractions that might have panicked 2022-09-29 15:53:54 +02:00
Alex 497164d782 Merge pull request 'Shutdown properly on SIGTERM/SIGHUP and on Windows signals' (#397) from handle-sigterm into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/397
2022-09-28 12:16:55 +02:00
Alex Auvolat 1f97ce37e6 Shutdown properly on SIGTERM/SIGHUP and on Windows signals 2022-09-28 10:41:59 +02:00
Alex Auvolat 0ab0d3cc29 Document db_engine 2022-09-27 16:52:36 +02:00
Alex 2197753dfd Merge pull request 'Add step to generate multi-arch Docker container in CI' (#393) from multi-arch-container into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/393
2022-09-27 11:55:49 +02:00
Alex 3f95a0f717 Merge pull request 'Enable k2v feature flag by default in CI' (#302) from k2v into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/302
2022-09-27 11:38:23 +02:00
Alex 7291747a28 Merge pull request 'Documentation changes for v0.8' (#394) from doc-0.8 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/394
2022-09-27 11:37:12 +02:00
Alex Auvolat d104ae8711 Add step to generate multi-arch Docker container in CI 2022-09-26 19:09:55 +02:00
Alex Auvolat 194e8be1bb Update docker image links 2022-09-26 18:01:17 +02:00
Alex Auvolat 69bcc813de Add garage v0.8 migration guide 2022-09-26 17:46:38 +02:00
Alex ea7a571d88 Merge pull request 'Fix span name for api server requests' (#392) from fix-span-name into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/392
2022-09-26 16:57:37 +02:00
Alex Auvolat 1778e4b318 Fix span name for api server requests 2022-09-26 16:21:30 +02:00
Alex af2b2f26b4 Merge pull request 'Update README (fix #230)' (#391) from new-readme into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/391
2022-09-21 13:28:52 +02:00
Alex Auvolat a3758dc4c4 Update README 2022-09-21 12:53:02 +02:00
Alex Auvolat e89f880694 Enable k2v feature flag in CI 2022-09-20 17:54:41 +02:00
Alex fc85508648 Merge pull request 'Initialize metrics exporter earlier (fix #389)' (#390) from fix-metrics into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/390
2022-09-20 17:53:46 +02:00
Alex Auvolat 782630fc27 Initialize metrics exporter earlier (fix #389) 2022-09-20 17:50:22 +02:00
Alex 7a901f7aab Merge pull request 'RPC performance changes' (#387) from configurable-timeouts into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/387
2022-09-20 16:17:23 +02:00
Alex Auvolat ded444f6c9 Ability to have custom timeouts in request strategy (not used) 2022-09-20 16:01:41 +02:00
Alex Auvolat 357b72f4ff Merge branch 'main' into configurable-timeouts 2022-09-20 15:19:58 +02:00
Alex 2c312e9cbd Merge pull request 'Change a warn! into a debug!' (#388) from less-noise into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/388
2022-09-20 11:57:52 +02:00
Alex Auvolat 1f7b050b7d Change a warn! into a debug! 2022-09-20 11:49:48 +02:00
Alex Auvolat 56592e1853 RPC performance changes
- configurable ping timeout
- single, much higher, configurable RPC timeout
- no more concurrency semaphore
2022-09-19 20:31:00 +02:00
Alex fbd32933ea Merge pull request 'Faster GetObject workflow for getting entire objects' (#386) from faster-get into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/386
2022-09-19 15:24:06 +02:00
Alex Auvolat 5d4b6f2173 Faster GetObject workflow for getting entire objects 2022-09-19 12:19:59 +02:00
Alex 4fba06d62e Merge pull request 'updates to documentation for v0.8' (#385) from doc-0.8 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/385
2022-09-19 10:45:10 +02:00
Alex Auvolat 1d0a610690 Finish writing about Garage features, and fix from-source instructions 2022-09-15 13:23:57 +02:00
Alex Auvolat f6aebefcc9 Some work on documentation towards v0.8 2022-09-14 19:31:13 +02:00
Alex 89b8087ba8 Merge pull request 'Properly return HTTP 204 when deleting non-existent object (fix #227)' (#384) from deleteobject-204 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/384
2022-09-14 17:16:39 +02:00
Alex Auvolat 76f42a1a2b Properly return HTTP 204 when deleting non-existent object (fix #227) 2022-09-14 17:07:55 +02:00
Alex 82600acf77 Merge pull request 'Allow for hostnames in bootstrap_peers and rpc_public_addr (fix #353)' (#383) from resolve-peer-names into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/383
2022-09-14 16:37:18 +02:00
Alex Auvolat e46dc2a8ef Allow for hostnames in bootstrap_peers and rpc_public_addr (fix #353) 2022-09-14 16:09:38 +02:00
Alex 80fdbfb0aa Merge pull request 'various fixes for v0.8.0' (#380) from various-fixes-for-0.8 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/380
2022-09-13 16:49:05 +02:00
Alex Auvolat ab722cb40f Add checks on replication_factor of layouts we use (fix #363, fix #364) 2022-09-13 16:22:23 +02:00
Alex Auvolat 38be811b1c Fix clippy lint that says we should implement Eq 2022-09-13 16:08:00 +02:00
Alex Auvolat 44733474bb Remove/change println! in server code (fix #358) 2022-09-13 16:01:55 +02:00
Alex Auvolat 07febd3ecd Ensure data dir is created immediately when Garage starts (fix #349) 2022-09-13 15:57:27 +02:00
Alex 11bdc971e2 Merge pull request 'use netapp streaming body' (#343) from netapp-stream-body into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/343
2022-09-13 15:26:08 +02:00
Alex Auvolat ff30891999 Use streaming block API for get with Range requests 2022-09-13 15:13:07 +02:00
Alex Auvolat 28a4af73ca Use netapp 0.5 published from crates.io 2022-09-13 13:11:44 +02:00
Alex Auvolat b823151a0b improvements in block manager 2022-09-12 16:57:38 +02:00
Alex 309d7aef3f Merge pull request 'performance improvements' (#342) from lx-perf-improvements into main
Performance improvements included in this PR:

- [x] Use `Bytes` at a few places where appropriate, instead of `Vec<u8>`, to reduce the number of copies
  - [x] StreamChunker now accumulates incoming slices in a `Vec<Bytes>` instead of a `VecDeque<u8>`. Replaces calls to `.extend()` and `.drain()` that were quite costly by a simple `concat()` on a vec of slices which is much more optimized
- [x] Hashing (b2, sha256, md5) is now done on a Tokio thread dedicated to cpu-intensive tasks, using `spawn_blocking`
- [x] Block manager now uses 256 independant locks instead of one big lock for writing, reduces contention when writing several/many objects in parallel
- [x] Better LMDB defaults: we now put flags `NoSync` and `NoMetaSync` to avoid `fsync` at each transaction (extremely slow). Also increased number of LMDB readers to accomodate more intensive workloads

Other changes included in this PR:

- [x] Update to hashing and MAC crates: md5 and sha2 from 0.9 to 0.10, hmac from 0.10 to 0.12
- [x] switch to `tracing_subscriber` for logs, which allows to have timing of each event

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/342
2022-09-12 16:38:43 +02:00
Alex Auvolat f91fab8582 Simplify+improve async hasher by using bounded channel 2022-09-12 16:23:43 +02:00
Alex Auvolat 7f54706b95 Merge branch 'lx-perf-improvements' into netapp-stream-body 2022-09-08 15:50:56 +02:00
Alex Auvolat d9d199a6c9 Merge branch 'main' into lx-perf-improvements 2022-09-08 15:49:17 +02:00
Alex 03c40a0b24 Merge pull request 'Reorganize dependencies' (#373) from improve-deps into main
This PR includes work from @jirutka :

- [x] Allow linking against system-provided libraries (libsodium, libsqlite, libzstd) #370
- [x] Make OTLP exporter optional and allow building without Prometheus exporter (/metrics) #372

And also:

- [x] Update `.nix` files
- [x] Remove heed default-features
- [x] Bump versions of all Garage crates to 0.8.0
- [x] Make db engines (lmdb, sled, sqlite) optionnal
- [x] Add documentation for available features
- [x] Directly include code of previous versions used for migration in order to reduce dependencies
- [x] Read variable `GIT_VERSION` from garage main instead of in crate garage_util to make builds faster
- [x] Report features used in the build somewhere? (in `garage --version` or something)
- [x] Check we `warn!` correctly if we try to use deactivated feature
- [x] Allow not to launch S3 endpoint if not in config

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/373
2022-09-08 15:45:09 +02:00
Alex Auvolat ceb1f0229a Move version back into util 2022-09-07 18:36:46 +02:00
Alex Auvolat f310fce34b Inject GIT_VERSION even later 2022-09-07 18:30:15 +02:00
Alex Auvolat 06df301de5 Fix merge 2022-09-07 18:16:01 +02:00
Alex Auvolat 8adc654713 Merge branch 'main' into improve-deps 2022-09-07 18:13:27 +02:00
Alex Auvolat 107853334b Fix build error 2022-09-07 18:10:19 +02:00
Alex Auvolat 1449204439 Add warnings when features are not included in build 2022-09-07 18:02:13 +02:00
Alex Auvolat 2e00809af5 Error messages when system-libs XOR bundled-libs != 1 2022-09-07 17:57:12 +02:00
Alex Auvolat 2559f63e9b Make all HTTP services optionnal 2022-09-07 17:54:16 +02:00
Alex Auvolat 28d86e7602 Report build features in garage --help 2022-09-07 17:05:21 +02:00
Alex Auvolat db61f41030 Move GIT_VERSION injection later in build chain to reduce build times 2022-09-07 11:59:56 +02:00
Alex Auvolat 907054775d Faster copy, better get error message 2022-09-06 22:25:23 +02:00
Alex Auvolat 6b958979bd Merge branch 'lx-perf-improvements' into netapp-stream-body 2022-09-06 22:13:01 +02:00
Alex Auvolat d23b3a14fc Merge branch 'main' into lx-perf-improvements 2022-09-06 21:53:37 +02:00
Alex Auvolat 4024822585 Update netapp to lastest git version with LAS scheduling 2022-09-06 19:45:00 +02:00
Alex Auvolat c2cc08852b Reenable node ordering 2022-09-06 19:31:42 +02:00
Alex Auvolat 6f02c36a89 cargo fmt 2022-09-06 17:59:41 +02:00
Alex Auvolat 0f5689c169 Include code from v0.5.1 directly to remove dependencies 2022-09-06 17:52:50 +02:00
Alex Auvolat 1e92e9f782 Disable k2v tests when feature is disabled 2022-09-06 17:29:46 +02:00
Alex Auvolat 431dee050f Remove opentelemetry-otlp dep in api/ 2022-09-06 17:25:44 +02:00
Alex Auvolat 2c2b93acdf Update Nix files with optional db engines 2022-09-06 17:20:10 +02:00
Alex Auvolat bbb970965c Document available build features 2022-09-06 17:16:45 +02:00
Alex Auvolat b886c75450 Make all DB engines optional build features 2022-09-06 17:09:43 +02:00
Alex Auvolat 48ffaaadfc Bump versions to 0.8.0 (compatibility is broken already) 2022-09-06 16:47:56 +02:00
Alex Auvolat 7de53a4d66 Force disable pkg-config for libsodum-sys and libzstd-sys 2022-09-06 16:41:58 +02:00
Alex Auvolat 8d77a76df1 Update .nix files 2022-09-06 15:49:41 +02:00
Alex Auvolat 454d8474ef Fix clippy 2022-09-06 15:43:50 +02:00
Alex ed7796924b Merge pull request 'Make OTLP exporter optional and allow building without Prometheus exporter (/metrics)' (#372) from jirutka/garage:telemetry-and-metrics into improve-deps
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/372
Reviewed-by: Alex <alex@adnab.me>
2022-09-06 15:11:30 +02:00
Jakub Jirutka ea36b9ff90 Allow building without Prometheus exporter (/metrics endpoint)
prometheus and opentelemetry-prometheus add 7 extra dependencies in
total and increases the size of the garage binary by ~7 % (with
fat LTO).
2022-09-06 01:15:09 +02:00
Jakub Jirutka e7af006c1c Make OTLP exporter optional via feature "telemetry-otlp"
opentelemetry-otlp add 48 (!) extra dependencies and increases the
size of the garage binary by ~11 % (with fat LTO).
2022-09-06 01:14:47 +02:00
Jakub Jirutka db72812f01 Use the new cargo feature resolver "2"
Garage currently uses the legacy resolver "1". The new one is used
by default if the root package specifies 'edition = 2021', which
Garage does not (yet).

The problem with the legacy resolver is, among others, that features
enabled by dev-dependencies are propagated to normal dependencies.
This affects e.g. hyper - one of the dev-dependencies enables "http2"
feature that adds many extra dependencies. If we build garage without
opentelemetry-otlp (this is enabled in the following commit), there's
no normal dependency enabling "http2" feature.

See https://doc.rust-lang.org/cargo/reference/resolver.html#feature-resolver-version-2
2022-09-06 01:14:19 +02:00
Alex Auvolat 729a910e14 Remove Heed default features 2022-09-05 16:40:13 +02:00
Alex 9f5433db82 Merge pull request 'Update .drone.yml signature' (#374) from fix-drone-signature into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/374
2022-09-05 16:18:15 +02:00
Alex Auvolat fd8074ad9b Update .drone.yml signature 2022-09-05 16:09:01 +02:00
Alex Auvolat 07e6bcde85 Merge branch 'main' into lx-perf-improvements 2022-09-05 12:40:17 +02:00
Alex 0009fd136c Merge pull request 'Make block resync speed dynamically configurable' (#369) from resync-ajustable-speed into main
Included in this PR:

- [x] Small refactor, resync code is moved to a separate `block/resync.rs` file
- [x] Block resync tranquility is no longer in config file, it is set dynamically using `garage worker set resync-tranquility` (this parameter is persisted over Garage restarts)
- [x] Up to 4 block resync workers can be activated to run simultaneously to speed up big resyncs, this parameter is set dynamically using `garage worker set resync-n-workers`

Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/369
2022-09-05 12:35:08 +02:00
Jakub Jirutka 7511ba5530 Allow linking against system-provided libsqlite
Unfortunately, rusqlite uses the opposite logic for enabling/disabling
bundled libraries to others (libsodium-sys, zstd-sys). Cargo features
are very limited and doesn't allow to enable feature A in a dependency
iff feature B is disabled.

Note, lmdb-rkv-sys doesn't need any special treatment because it
automatically links against system liblmdb if found via pkgconf.

Linux distros should build garage with
`--no-default-features --features system-libs` to disable bundled-libs
and enable system-libs.
2022-09-03 19:15:57 +02:00
Jakub Jirutka a6e40b75ea Add feature "system-libs" to enable linking against system libraries
If this feature is enabled, libsodium-sys and zstd-sys will link
dynamically against system-provided libraries instead of building
and linking statically the bundled (possibly outdated and vulnerable)
copies of them. This feature is intended mainly for linux package
maintainers.
2022-09-03 18:44:34 +02:00
Alex Auvolat e1751c8a9c fix clippy 2022-09-02 17:24:26 +02:00
Alex Auvolat 5d4b937a00 Ability to have up to 4 concurrently working resync workers 2022-09-02 17:18:13 +02:00
Alex Auvolat 5e8baa433d Make BlockManagerLocked fully private again 2022-09-02 16:52:22 +02:00
Alex Auvolat 47be652a1f block manager: refactor: split resync into separate file 2022-09-02 16:47:15 +02:00
Alex Auvolat 943d76c583 Ability to dynamically set resync tranquility 2022-09-02 15:34:21 +02:00
Alex Auvolat 6226f5ceca Update to netapp 0.4.5 - fixed ping 2022-09-02 14:33:12 +02:00
Alex Auvolat 13b5f28c7e Make use of BytesBuf from new Netapp 2022-09-02 13:46:42 +02:00
Alex Auvolat 1ef87ac4cb cargo fmt 2022-09-02 13:38:29 +02:00
Alex Auvolat 99b532b85b Apply PRIO_SECONDARY to block data transfers 2022-09-01 16:35:43 +02:00
Alex Auvolat e648bf7b69 update cargo.nix 2022-09-01 16:31:04 +02:00
Alex Auvolat df094bd807 Less strict timeouts 2022-09-01 16:30:44 +02:00
Alex Auvolat f3bf34b6a1 update netapp: straming + fix-ping 2022-09-01 14:23:54 +02:00
Alex Auvolat bc977f9a7a Update to Netapp with OrderTag support and exploit OrderTags 2022-09-01 12:58:20 +02:00
Alex Auvolat 4b726b0941 netapp recv with unbounded channel removes deadlock 2022-09-01 09:47:28 +02:00
Alex Auvolat 70231d68b2 Fix bytes_read counter 2022-08-31 19:44:27 +02:00
Alex Auvolat e598231ca4 update netapp git commit 2022-08-31 19:27:25 +02:00
Alex Auvolat c9bc9d89de Merge branch 'lx-perf-improvements' into netapp-stream-body 2022-08-31 17:42:31 +02:00
Alex Auvolat eb97e13a6a update cargo.nix 2022-08-31 17:42:00 +02:00
Alex Auvolat efbca67ce4 Add env filter to tracing subscriber 2022-08-31 14:39:12 +02:00
Alex Auvolat 44cd98d2e4 Tracing-subscriber: write to stderr 2022-08-31 14:28:17 +02:00
Alex Auvolat dd5304f6fc Replace logging crate pretty_env_logger by tracing_subscriber::fmt 2022-08-31 14:24:41 +02:00
Alex Auvolat 322dafc761 Try to fix clippy 2022-08-29 17:32:45 +02:00
Alex Auvolat 5d065b8a0f cargo2nix fix to fetchCrateGit 2022-08-29 17:24:53 +02:00
Alex Auvolat 52749e28f7 Merge branch 'lx-perf-improvements' into netapp-stream-body 2022-08-29 16:48:43 +02:00
Alex Auvolat 4da67b0035 Update drone signature 2022-08-29 16:48:31 +02:00
Alex Auvolat 1921f4f7e6 Merge branch 'lx-perf-improvements' into netapp-stream-body 2022-08-29 16:45:05 +02:00
Alex Auvolat ebc20a8798 Merge branch 'main' into lx-perf-improvements 2022-08-29 16:44:13 +02:00
Quentin Dufour 532eca7ff9 Add some documentation for Caddy 2022-08-12 10:33:41 +02:00
Quentin Dufour 2c7bae935a Configure structopt to report the right version
By default, structopt reports the value provided by
the env var CARGO_PKG_VERSION, feeded by Cargo when reading
Cargo.toml. However for Garage we use a versioning based on git,
so we often report a version that is behind the real version.
In this commit, we create garage_util::version::garage() that
reports the right version and configure all structopt subcommands
to call this function instead of using the env var.
2022-08-11 10:21:45 +02:00
Alex Auvolat e935861854 Factor out node request order selection logic & use in manager 2022-07-29 12:25:03 +02:00
Alex Auvolat f0ee3056d3 Update cargo.nix 2022-07-29 12:25:03 +02:00
Alex Auvolat 126b037307 update netapp 2022-07-29 12:25:03 +02:00
Alex Auvolat 33750c04ed Update cargo.nix 2022-07-29 12:25:03 +02:00
Alex Auvolat 68087ee13d Fix clippy 2022-07-29 12:25:03 +02:00
Alex Auvolat 605a630333 Use streaming in block manager 2022-07-29 12:25:02 +02:00
Alex Auvolat a35d4da721 update netapp to 0.5 2022-07-29 12:25:02 +02:00
Alex Auvolat 8e7e680afe First adaptation to WIP netapp with streaming body 2022-07-29 12:25:02 +02:00
Alex Auvolat 16f6a1a65d fix clippy 2022-07-29 12:24:49 +02:00
Alex Auvolat ad35b18bb1 Faster chunker 2022-07-29 12:24:49 +02:00
Alex Auvolat 49154a78d8 Update cargo.nix 2022-07-29 12:24:48 +02:00
Alex Auvolat ff4771c36a cargo fmt 2022-07-29 12:24:48 +02:00
Alex Auvolat 381eb9a5a1 Fix tests 2022-07-29 12:24:48 +02:00
Alex Auvolat 2cad656a03 More make clippy happy 2022-07-29 12:24:48 +02:00
Alex Auvolat 0176da3ad2 Make clippy happy 2022-07-29 12:24:48 +02:00
Alex Auvolat 40150527b8 Update cargo.nix 2022-07-29 12:24:48 +02:00
Alex Auvolat 2f111e6b3d Performance improvements:
- reduce contention on mutation_lock by having 256 of them
- better lmdb defaults
2022-07-29 12:24:48 +02:00
Alex Auvolat 1b2e1296eb Compute hashes on dedicated threads 2022-07-29 12:24:44 +02:00
137 changed files with 7255 additions and 4577 deletions
+26 -12
View File
@@ -14,14 +14,16 @@ steps:
- name: build
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version $DRONE_COMMIT
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
- name: unit + func tests
image: nixpkgs/nix:nixos-22.05
environment:
GARAGE_TEST_INTEGRATION_EXE: result/bin/garage
GARAGE_TEST_INTEGRATION_EXE: result-bin/bin/garage
commands:
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
- nix-build --no-build-output --attr test.amd64
- ./result/bin/garage_db-*
- ./result/bin/garage_api-*
- ./result/bin/garage_model-*
- ./result/bin/garage_rpc-*
@@ -30,11 +32,12 @@ steps:
- ./result/bin/garage_web-*
- ./result/bin/garage-*
- ./result/bin/integration-*
- rm result
- name: integration tests
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version $DRONE_COMMIT
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
- nix-shell --attr integration --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
trigger:
@@ -57,8 +60,8 @@ steps:
- name: build
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build --no-build-output --attr pkgs.amd64.release --argstr git_version $DRONE_COMMIT
- nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
- nix-build --no-build-output --attr pkgs.amd64.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
- nix-shell --attr rust --run "./script/not-dynamic.sh result-bin/bin/garage"
- name: integration
image: nixpkgs/nix:nixos-22.05
@@ -108,8 +111,8 @@ steps:
- name: build
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build --no-build-output --attr pkgs.i386.release --argstr git_version $DRONE_COMMIT
- nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
- nix-build --no-build-output --attr pkgs.i386.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
- nix-shell --attr rust --run "./script/not-dynamic.sh result-bin/bin/garage"
- name: integration
image: nixpkgs/nix:nixos-22.05
@@ -158,8 +161,8 @@ steps:
- name: build
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build --no-build-output --attr pkgs.arm64.release --argstr git_version $DRONE_COMMIT
- nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
- nix-build --no-build-output --attr pkgs.arm64.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
- nix-shell --attr rust --run "./script/not-dynamic.sh result-bin/bin/garage"
- name: push static binary
image: nixpkgs/nix:nixos-22.05
@@ -203,8 +206,8 @@ steps:
- name: build
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build --no-build-output --attr pkgs.arm.release --argstr git_version $DRONE_COMMIT
- nix-shell --attr rust --run "./script/not-dynamic.sh result/bin/garage"
- nix-build --no-build-output --attr pkgs.arm.release --argstr git_version ${DRONE_TAG:-$DRONE_COMMIT}
- nix-shell --attr rust --run "./script/not-dynamic.sh result-bin/bin/garage"
- name: push static binary
image: nixpkgs/nix:nixos-22.05
@@ -245,6 +248,17 @@ node:
nix-daemon: 1
steps:
- name: multiarch-docker
image: nixpkgs/nix:nixos-22.05
environment:
DOCKER_AUTH:
from_secret: docker_auth
HOME: "/root"
commands:
- mkdir -p /root/.docker
- echo $DOCKER_AUTH > /root/.docker/config.json
- export CONTAINER_TAG=${DRONE_TAG:-$DRONE_COMMIT}
- nix-shell --attr release --run "multiarch_docker"
- name: refresh-index
image: nixpkgs/nix:nixos-22.05
environment:
@@ -269,6 +283,6 @@ trigger:
---
kind: signature
hmac: ccec0e06bf6676a705d6a4e63dc322691148bc72e72a4aaa54be6c5fd22652a2
hmac: ac09a5a8c82502f67271f93afa1e1e21ce66383b8e24a6deb26b285cc1c378ba
...
Generated
+617 -536
View File
File diff suppressed because it is too large Load Diff
+2289 -2057
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1,4 +1,5 @@
[workspace]
resolver = "2"
members = [
"src/db",
"src/util",
+1 -1
View File
@@ -3,5 +3,5 @@ FROM scratch
ENV RUST_BACKTRACE=1
ENV RUST_LOG=garage=info
COPY result/bin/garage /
COPY result-bin/bin/garage /
CMD [ "/garage", "server"]
+1 -1
View File
@@ -1,7 +1,7 @@
.PHONY: doc all release shell run1 run2 run3
all:
clear; cargo build --all-features
clear; cargo build
release:
nix-build --arg release true
+17 -11
View File
@@ -15,18 +15,24 @@ Garage [![Build Status](https://drone.deuxfleurs.fr/api/badges/Deuxfleurs/garage
]
</p>
Garage is a lightweight S3-compatible distributed object store, with the following goals:
Garage is an S3-compatible distributed object storage service
designed for self-hosting at a small-to-medium scale.
- As self-contained as possible
- Easy to set up
- Highly resilient to network failures, network latency, disk failures, sysadmin failures
- Relatively simple
- Made for multi-datacenter deployments
Garage is designed for storage clusters composed of nodes running
at different physical locations,
in order to easily provide a storage service that replicates data at these different
locations and stays available even when some servers are unreachable.
Garage also focuses on being lightweight, easy to operate, and highly resilient to
machine failures.
Non-goals include:
Garage is built by [Deuxfleurs](https://deuxfleurs.fr),
an experimental small-scale self hosted service provider,
which has been using it in production since its first release in 2020.
- Extremely high performance
- Complete implementation of the S3 API
- Erasure coding (our replication model is simply to copy the data as is on several nodes, in different datacenters if possible)
Learn more on our dedicated documentation pages:
Our main use case is to provide a distributed storage layer for small-scale self hosted services such as [Deuxfleurs](https://deuxfleurs.fr).
- [Goals and use cases](https://garagehq.deuxfleurs.fr/documentation/design/goals/)
- [Features](https://garagehq.deuxfleurs.fr/documentation/reference-manual/features/)
- [Quick start](https://garagehq.deuxfleurs.fr/documentation/quick-start/)
Garage is entirely free software released under the terms of the AGPLv3.
+34 -5
View File
@@ -5,13 +5,26 @@
with import ./nix/common.nix;
let
let
pkgs = import pkgsSrc { };
compile = import ./nix/compile.nix;
build_debug_and_release = (target: {
debug = (compile { inherit target; release = false; }).workspace.garage { compileMode = "build"; };
release = (compile { inherit target; release = true; }).workspace.garage { compileMode = "build"; };
debug = (compile {
inherit target git_version;
release = false;
}).workspace.garage {
compileMode = "build";
};
release = (compile {
inherit target git_version;
release = true;
}).workspace.garage {
compileMode = "build";
};
});
test = (rustPkgs: pkgs.symlinkJoin {
name ="garage-tests";
paths = builtins.map (key: rustPkgs.workspace.${key} { compileMode = "test"; }) (builtins.attrNames rustPkgs.workspace);
@@ -25,9 +38,25 @@ in {
arm = build_debug_and_release "armv6l-unknown-linux-musleabihf";
};
test = {
amd64 = test (compile { target = "x86_64-unknown-linux-musl"; });
amd64 = test (compile {
inherit git_version;
target = "x86_64-unknown-linux-musl";
features = [
"garage/bundled-libs"
"garage/k2v"
"garage/sled"
"garage/lmdb"
"garage/sqlite"
];
});
};
clippy = {
amd64 = (compile { compiler = "clippy"; }).workspace.garage { compileMode = "build"; } ;
amd64 = (compile {
inherit git_version;
target = "x86_64-unknown-linux-musl";
compiler = "clippy";
}).workspace.garage {
compileMode = "build";
};
};
}
+3 -1
View File
@@ -5,12 +5,14 @@ weight = 25
## Configuring a bucket for website access
There are two methods to expose buckets as website:
There are three methods to expose buckets as website:
1. using the PutBucketWebsite S3 API call, which is allowed for access keys that have the owner permission bit set
2. from the Garage CLI, by an adminstrator of the cluster
3. using the Garage administration API
The `PutBucketWebsite` API endpoint [is documented](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketWebsite.html) in the official AWS docs.
This endpoint can also be called [using `aws s3api`](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-website.html) on the command line.
The website configuration supported by Garage is only a subset of the possibilities on Amazon S3: redirections are not supported, only the index document and error document can be specified.
+62 -26
View File
@@ -20,40 +20,76 @@ sudo apt-get update
sudo apt-get install build-essential
```
## Using source from `crates.io`
Garage's source code is published on `crates.io`, Rust's official package repository.
This means you can simply ask `cargo` to download and build this source code for you:
```bash
cargo install garage
```
That's all, `garage` should be in `$HOME/.cargo/bin`.
You can add this folder to your `$PATH` or copy the binary somewhere else on your system.
For instance:
```bash
sudo cp $HOME/.cargo/bin/garage /usr/local/bin/garage
```
## Using source from the Gitea repository
## Building from source from the Gitea repository
The primary location for Garage's source code is the
[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage).
[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage),
which contains all of the released versions as well as the code
for the developpement of the next version.
Clone the repository and build Garage with the following commands:
Clone the repository and enter it as follows:
```bash
git clone https://git.deuxfleurs.fr/Deuxfleurs/garage.git
cd garage
cargo build
```
Be careful, as this will make a debug build of Garage, which will be extremely slow!
To make a release build, invoke `cargo build --release` (this takes much longer).
If you wish to build a specific version of Garage, check out the corresponding tag. For instance:
The binaries built this way are found in `target/{debug,release}/garage`.
```bash
git tag # List available tags
git checkout v0.8.0 # Change v0.8.0 with the version you wish to build
```
Otherwise you will be building a developpement build from the `main` branch
that includes all of the changes to be released in the next version.
Be careful that such a build might be unstable or contain bugs,
and could be incompatible with nodes that run stable versions of Garage.
Finally, build Garage with the following command:
```bash
cargo build --release
```
The binary built this way can now be found in `target/release/garage`.
You may simply copy this binary to somewhere in your `$PATH` in order to
have the `garage` command available in your shell, for instance:
```bash
sudo cp target/release/garage /usr/local/bin/garage
```
If you are planning to develop Garage,
you might be interested in producing debug builds, which compile faster but run slower:
this can be done by removing the `--release` flag, and the resulting build can then
be found in `target/debug/garage`.
## List of available Cargo feature flags
Garage supports a number of compilation options in the form of Cargo feature flags,
which can be used to provide builds adapted to your system and your use case.
To produce a build with a given set of features, invoke the `cargo build` command
as follows:
```bash
# This will build the default feature set plus feature1, feature2 and feature3
cargo build --release --features feature1,feature2,feature3
# This will build ONLY feature1, feature2 and feature3
cargo build --release --no-default-features \
--features feature1,feature2,feature3
```
The following feature flags are available in v0.8.0:
| Feature flag | Enabled | Description |
| ------------ | ------- | ----------- |
| `bundled-libs` | *by default* | Use bundled version of sqlite3, zstd, lmdb and libsodium |
| `system-libs` | optional | Use system version of sqlite3, zstd, lmdb and libsodium<br>if available (exclusive with `bundled-libs`, build using<br>`cargo build --no-default-features --features system-libs`) |
| `k2v` | optional | Enable the experimental K2V API (if used, all nodes on your<br>Garage cluster must have it enabled as well) |
| `kubernetes-discovery` | optional | Enable automatic registration and discovery<br>of cluster nodes through the Kubernetes API |
| `metrics` | *by default* | Enable collection of metrics in Prometheus format on the admin API |
| `telemetry-otlp` | optional | Enable collection of execution traces using OpenTelemetry |
| `sled` | *by default* | Enable using Sled to store Garage's metadata |
| `lmdb` | optional | Enable using LMDB to store Garage's metadata |
| `sqlite` | optional | Enable using Sqlite3 to store Garage's metadata |
+87
View File
@@ -0,0 +1,87 @@
+++
title = "Deploying on Kubernetes"
weight = 32
+++
Garage can also be deployed on a kubernetes cluster via helm chart.
## Deploying
Firstly clone the repository:
```bash
git clone https://git.deuxfleurs.fr/Deuxfleurs/garage
cd garage/scripts/helm
```
Deploy with default options:
```bash
helm install --create-namespace --namespace garage garage ./garage
```
Or deploy with custom values:
```bash
helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml
```
After deploying, cluster layout must be configured manually as described in [Creating a cluster layout](@/documentation/quick-start/_index.md#creating-a-cluster-layout). Use the following command to access garage CLI:
```bash
kubectl exec --stdin --tty -n garage garage-0 -- ./garage status
```
## Overriding default values
All possible configuration values can be found with:
```bash
helm show values ./garage
```
This is an example `values.overrride.yaml` for deploying in a microk8s cluster with a https s3 api ingress route:
```yaml
garage:
# Use only 2 replicas per object
replicationMode: "2"
# Start 4 instances (StatefulSets) of garage
replicaCount: 4
# Override default storage class and size
persistence:
meta:
storageClass: "openebs-hostpath"
size: 100Mi
data:
storageClass: "openebs-hostpath"
size: 1Gi
ingress:
s3:
api:
enabled: true
className: "public"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/proxy-body-size: 500m
hosts:
- host: s3-api.my-domain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: garage-ingress-cert
hosts:
- s3-api.my-domain.com
```
## Removing
```bash
helm delete --namespace garage garage
```
Note that this will leave behind custom CRD `garagenodes.deuxfleurs.fr`, which must be removed manually if desired.
+6 -6
View File
@@ -51,15 +51,15 @@ to store 2 TB of data in total.
## Get a Docker image
Our docker image is currently named `dxflrs/amd64_garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/amd64_garage/tags?page=1&ordering=last_updated).
We encourage you to use a fixed tag (eg. `v0.4.0`) and not the `latest` tag.
For this example, we will use the latest published version at the time of the writing which is `v0.4.0` but it's up to you
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/amd64_garage/tags?page=1&ordering=last_updated).
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
We encourage you to use a fixed tag (eg. `v0.8.0`) and not the `latest` tag.
For this example, we will use the latest published version at the time of the writing which is `v0.8.0` but it's up to you
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
For example:
```
sudo docker pull dxflrs/amd64_garage:v0.4.0
sudo docker pull dxflrs/garage:v0.8.0
```
## Deploying and configuring Garage
@@ -125,7 +125,7 @@ docker run \
-v /etc/garage.toml:/etc/garage.toml \
-v /var/lib/garage/meta:/var/lib/garage/meta \
-v /var/lib/garage/data:/var/lib/garage/data \
lxpz/garage_amd64:v0.4.0
dxflrs/garage:v0.8.0
```
It should be restarted automatically at each reboot.
+22
View File
@@ -280,3 +280,25 @@ Traefik's caching middleware is only available on [entreprise version](https://d
[http.middlewares]
[http.middlewares.gzip_compress.compress]
```
## Caddy
Your Caddy configuration can be as simple as:
```caddy
s3.garage.tld, *.s3.garage.tld {
reverse_proxy localhost:3900 192.168.1.2:3900 example.tld:3900
}
*.web.garage.tld {
reverse_proxy localhost:3902 192.168.1.2:3900 example.tld:3900
}
admin.garage.tld {
reverse_proxy localhost:3903
}
```
But at the same time, the `reverse_proxy` is very flexible.
For a production deployment, you should [read its documentation](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy) as it supports features like DNS discovery of upstreams, load balancing with checks, streaming parameters, etc.
+1 -1
View File
@@ -1,6 +1,6 @@
+++
title = "Benchmarks"
weight = 10
weight = 40
+++
With Garage, we wanted to build a software defined storage service that follow the [KISS principle](https://en.wikipedia.org/wiki/KISS_principle),
+3 -3
View File
@@ -1,23 +1,23 @@
+++
title = "Goals and use cases"
weight = 5
weight = 10
+++
## Goals and non-goals
Garage is a lightweight geo-distributed data store that implements the
[Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html)
object storage protocole. It enables applications to store large blobs such
object storage protocol. It enables applications to store large blobs such
as pictures, video, images, documents, etc., in a redundant multi-node
setting. S3 is versatile enough to also be used to publish a static
website.
Garage is an opinionated object storage solutoin, we focus on the following **desirable properties**:
- **Internet enabled**: made for multi-sites (eg. datacenters, offices, households, etc.) interconnected through regular Internet connections.
- **Self-contained & lightweight**: works everywhere and integrates well in existing environments to target [hyperconverged infrastructures](https://en.wikipedia.org/wiki/Hyper-converged_infrastructure).
- **Highly resilient**: highly resilient to network failures, network latency, disk failures, sysadmin failures.
- **Simple**: simple to understand, simple to operate, simple to debug.
- **Internet enabled**: made for multi-sites (eg. datacenters, offices, households, etc.) interconnected through regular Internet connections.
We also noted that the pursuit of some other goals are detrimental to our initial goals.
The following has been identified as **non-goals** (if these points matter to you, you should not use Garage):
+43
View File
@@ -20,6 +20,49 @@ In the meantime, you can find some information at the following links:
- [an old design draft](@/documentation/working-documents/design-draft.md)
## Request routing logic
Data retrieval requests to Garage endpoints (S3 API and websites) are resolved
to an individual object in a bucket. Since objects are replicated to multiple nodes
Garage must ensure consistency before answering the request.
### Using quorum to ensure consistency
Garage ensures consistency by attempting to establish a quorum with the
data nodes responsible for the object. When a majority of the data nodes
have provided metadata on a object Garage can then answer the request.
When a request arrives Garage will, assuming the recommended 3 replicas, perform the following actions:
- Make a request to the two preferred nodes for object metadata
- Try the third node if one of the two initial requests fail
- Check that the metadata from at least 2 nodes match
- Check that the object hasn't been marked deleted
- Answer the request with inline data from metadata if object is small enough
- Or get data blocks from the preferred nodes and answer using the assembled object
Garage dynamically determines which nodes to query based on health, preference, and
which nodes actually host a given data. Garage has no concept of "primary" so any
healthy node with the data can be used as long as a quorum is reached for the metadata.
### Node health
Garage keeps a TCP session open to each node in the cluster and periodically pings them. If a connection
cannot be established, or a node fails to answer a number of pings, the target node is marked as failed.
Failed nodes are not used for quorum or other internal requests.
### Node preference
Garage prioritizes which nodes to query according to a few criteria:
- A node always prefers itself if it can answer the request
- Then the node prioritizes nodes in the same zone
- Finally the nodes with the lowest latency are prioritized
For further reading on the cluster structure look at the [gateway](@/documentation/cookbook/gateways.md)
and [cluster layout management](@/documentation/reference-manual/layout.md) pages.
## Garbage collection
A faulty garbage collection procedure has been the cause of
+1 -1
View File
@@ -1,6 +1,6 @@
+++
title = "Related work"
weight = 15
weight = 50
+++
## Context
+9
View File
@@ -9,6 +9,15 @@ Let's start your Garage journey!
In this chapter, we explain how to deploy Garage as a single-node server
and how to interact with it.
## What is Garage?
Before jumping in, you might be interested in reading the following pages:
- [Goals and use cases](@/documentation/design/goals.md)
- [List of features](@/documentation/reference-manual/features.md)
## Scope of this tutorial
Our goal is to introduce you to Garage's workflows.
Following this guide is recommended before moving on to
[configuring a multi-node cluster](@/documentation/cookbook/real-world.md).
+1 -1
View File
@@ -1,6 +1,6 @@
+++
title = "Administration API"
weight = 16
weight = 60
+++
The Garage administration API is accessible through a dedicated server whose
+1 -1
View File
@@ -1,6 +1,6 @@
+++
title = "Garage CLI"
weight = 15
weight = 30
+++
The Garage CLI is mostly self-documented. Make use of the `help` subcommand
+108 -44
View File
@@ -1,6 +1,6 @@
+++
title = "Configuration file format"
weight = 5
weight = 20
+++
Here is an example `garage.toml` configuration file that illustrates all of the possible options:
@@ -9,8 +9,12 @@ Here is an example `garage.toml` configuration file that illustrates all of the
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "lmdb"
block_size = 1048576
block_manager_background_tranquility = 2
sled_cache_capacity = 134217728
sled_flush_every_ms = 2000
replication_mode = "3"
@@ -27,15 +31,20 @@ bootstrap_peers = [
"212fd62eeaca72c122b45a7f4fa0f55e012aa5e24ac384a72a3016413fa724ff@[fc00:F::1]:3901",
]
consul_host = "consul.service"
consul_service_name = "garage-daemon"
kubernetes_namespace = "garage"
kubernetes_service_name = "garage-daemon"
kubernetes_skip_crd = false
[consul_discovery]
consul_http_addr = "http://127.0.0.1:8500"
service_name = "garage-daemon"
ca_cert = "/etc/consul/consul-ca.crt"
client_cert = "/etc/consul/consul-client.crt"
client_key = "/etc/consul/consul-key.crt"
tls_skip_verify = false
[kubernetes_discovery]
namespace = "garage"
service_name = "garage-daemon"
skip_crd = false
sled_cache_capacity = 134217728
sled_flush_every_ms = 2000
[s3_api]
api_bind_addr = "[::]:3900"
@@ -72,6 +81,47 @@ 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).
### `db_engine` (since `v0.8.0`)
By default, Garage uses the Sled embedded database library
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 traditionnal 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.
### `block_size`
Garage splits stored objects in consecutive chunks of size `block_size`
@@ -87,16 +137,20 @@ files will remain available. This however means that chunks from existing files
will not be deduplicated with chunks from newly uploaded files, meaning you
might use more storage space that is optimally possible.
### `block_manager_background_tranquility`
### `sled_cache_capacity`
This parameter tunes the activity of the background worker responsible for
resyncing data blocks between nodes. The higher the tranquility value is set,
the more the background worker will wait between iterations, meaning the load
on the system (including network usage between nodes) will be reduced. The
minimal value for this parameter is `0`, where the background worker will
allways work at maximal throughput to resynchronize blocks. The default value
is `2`, where the background worker will try to spend at most 1/3 of its time
working, and 2/3 sleeping in order to reduce system load.
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.
### `replication_mode`
@@ -245,48 +299,58 @@ be obtained by running `garage node id` and then included directly in the
key will be returned by `garage node id` and you will have to add the IP
yourself.
### `consul_host` and `consul_service_name`
## The `[consul_discovery]` section
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
reached by other nodes of the cluster, which should be set in `rpc_public_addr`.
The `consul_host` parameter should be set to the hostname of the Consul server,
and `consul_service_name` should be set to the service name under which Garage's
### `consul_http_addr` and `service_name`
The `consul_http_addr` parameter should be set to the full HTTP(S) address of the Consul server.
### `service_name`
`service_name` should be set to the service name under which Garage's
RPC ports are announced.
Garage does not yet support talking to Consul over TLS.
### `client_cert`, `client_key`
### `kubernetes_namespace`, `kubernetes_service_name` and `kubernetes_skip_crd`
TLS client certificate and client key to use when communicating with Consul over TLS. Both are mandatory when doing so.
### `ca_cert`
TLS CA certificate to use when communicating with Consul over TLS.
### `tls_skip_verify`
Skip server hostname verification in TLS handshake.
`ca_cert` is ignored when this is set.
## The `[kubernetes_discovery]` section
Garage supports discovering other nodes of the cluster using kubernetes custom
resources. For this to work `kubernetes_namespace` and `kubernetes_service_name`
need to be configured.
resources. For this to work, a `[kubernetes_discovery]` section must be present
with at least the `namespace` and `service_name` parameters.
`kubernetes_namespace` sets the namespace in which the custom resources are
configured. `kubernetes_service_name` is added as a label to these resources to
### `namespace`
`namespace` sets the namespace in which the custom resources are
configured.
### `service_name`
`service_name` is added as a label to the advertised resources to
filter them, to allow for multiple deployments in a single namespace.
`kubernetes_skip_crd` can be set to true to disable the automatic creation and
### `skip_crd`
`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
manually.
### `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.
## The `[s3_api]` section
+125
View File
@@ -0,0 +1,125 @@
+++
title = "List of Garage features"
weight = 10
+++
### S3 API
The main goal of Garage is to provide an object storage service that is compatible with the
[S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) from Amazon Web Services.
We try to adhere as strictly as possible to the semantics of the API as implemented by Amazon
and other vendors such as Minio or CEPH.
Of course Garage does not implement the full span of API endpoints that AWS S3 does;
the exact list of S3 features implemented by Garage can be found [on our S3 compatibility page](@/documentation/reference-manual/s3-compatibility.md).
### Geo-distribution
Garage allows you to store copies of your data in multiple geographical locations in order to maximize resilience
to adverse events, such as network/power outages or hardware failures.
This allows Garage to run very well even at home, using consumer-grade Internet connectivity
(such as FTTH) and power, as long as cluster nodes can be spawned at several physical locations.
Garage exploits knowledge of the capacity and physical location of each storage node to design
a storage plan that best exploits the available storage capacity while satisfying the geo-distributed replication constraint.
To learn more about geo-distributed Garage clusters,
read our documentation on [setting up a real-world deployment](@/documentation/cookbook/real-world.md).
### Standalone/self-contained
Garage is extremely simple to deploy, and does not depend on any external service to run.
This makes setting up and administering storage clusters, we hope, as easy as it could be.
### Flexible topology
A Garage cluster can very easily evolve over time, as storage nodes are added or removed.
Garage will automatically rebalance data between nodes as needed to ensure the desired number of copies.
Read about cluster layout management [here](@/documentation/reference-manual/layout.md).
### No RAFT slowing you down
It might seem strange to tout the absence of something as a desirable feature,
but this is in fact a very important point! Garage does not use RAFT or another
consensus algorithm internally to order incoming requests: this means that all requests
directed to a Garage cluster can be handled independently of one another instead
of going through a central bottleneck (the leader node).
As a consequence, requests can be handled much faster, even in cases where latency
between cluster nodes is important (see our [benchmarks](@/documentation/design/benchmarks/index.md) for data on this).
This is particularly usefull when nodes are far from one another and talk to one other through standard Internet connections.
### Several replication modes
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.
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).
### Web server for static websites
A storage bucket can easily be configured to be served directly by Garage as a static web site.
Domain names for multiple websites directly map to bucket names, making it easy to build
a platform for your users to autonomously build and host their websites over Garage.
Surprisingly, none of the other alternative S3 implementations we surveyed (such as Minio
or CEPH) support publishing static websites from S3 buckets, a feature that is however
directly inherited from S3 on AWS.
Read more on our [dedicated documentation page](@/documentation/cookbook/exposing-websites.md).
### Bucket names as aliases
In Garage, a bucket may have several names, known as aliases.
Aliases can easily be added and removed on demand:
this allows to easily rename buckets if needed
without having to copy all of their content, something that cannot be done on AWS.
For buckets served as static websites, having multiple aliases for a bucket can allow
exposing the same content under different domain names.
Garage also supports bucket aliases which are local to a single user:
this allows different users to have different buckets with the same name, thus avoiding naming collisions.
This can be helpfull for instance if you want to write an application that creates per-user buckets with always the same name.
This feature is totally invisible to S3 clients and does not break compatibility with AWS.
### Cluster administration API
Garage provides a fully-fledged REST API to administer your cluster programatically.
Functionnality included in the admin API include: setting up and monitoring
cluster nodes, managing access credentials, and managing storage buckets and bucket aliases.
A full reference of the administration API is available [here](@/documentation/reference-manual/admin-api.md).
### Metrics and traces
Garage makes some internal metrics available in the Prometheus data format,
which allows you to build interactive dashboards to visualize the load and internal state of your storage cluster.
For developpers and performance-savvy administrators,
Garage also supports exporting traces of what it does internally in OpenTelemetry format.
This allows to monitor the time spent at various steps of the processing of requests,
in order to detect potential performance bottlenecks.
### Kubernetes and Nomad integrations
Garage can automatically discover other nodes in the cluster thanks to integration
with orchestrators such as Kubernetes and Nomad (when used with Consul).
This eases the configuration of your cluster as it removes one step where nodes need
to be manually connected to one another.
### Support for changing IP addresses
As long as all of your nodes don't change their IP address at the same time,
Garage should be able to tolerate nodes with changing/dynamic IP addresses,
as nodes will regularly exchange the IP addresses of their peers and try to
reconnect using newer addresses when existing connections are broken.
### K2V API (experimental)
As part of an ongoing research project, Garage can expose an experimental key/value storage API called K2V.
K2V is made for the storage and retrieval of many small key/value pairs that need to be processed in bulk.
This completes the S3 API with an alternative that can be used to easily store and access metadata
related to objects stored in an S3 bucket.
In the context of our research project, [Aérogramme](https://aerogramme.deuxfleurs.fr),
K2V is used to provide metadata and log storage for operations on encrypted e-mail storage.
Learn more on the specification of K2V [here](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/k2v/doc/drafts/k2v-spec.md)
and on how to enable it in Garage [here](@/documentation/reference-manual/k2v.md).
+1 -1
View File
@@ -1,6 +1,6 @@
+++
title = "K2V"
weight = 30
weight = 70
+++
Starting with version 0.7.2, Garage introduces an optionnal feature, K2V,
+1 -1
View File
@@ -1,6 +1,6 @@
+++
title = "Cluster layout management"
weight = 10
weight = 50
+++
The cluster layout in Garage is a table that assigns to each node a role in
-45
View File
@@ -1,45 +0,0 @@
+++
title = "Request routing logic"
weight = 10
+++
Data retrieval requests to Garage endpoints (S3 API and websites) are resolved
to an individual object in a bucket. Since objects are replicated to multiple nodes
Garage must ensure consistency before answering the request.
## Using quorum to ensure consistency
Garage ensures consistency by attempting to establish a quorum with the
data nodes responsible for the object. When a majority of the data nodes
have provided metadata on a object Garage can then answer the request.
When a request arrives Garage will, assuming the recommended 3 replicas, perform the following actions:
- Make a request to the two preferred nodes for object metadata
- Try the third node if one of the two initial requests fail
- Check that the metadata from at least 2 nodes match
- Check that the object hasn't been marked deleted
- Answer the request with inline data from metadata if object is small enough
- Or get data blocks from the preferred nodes and answer using the assembled object
Garage dynamically determines which nodes to query based on health, preference, and
which nodes actually host a given data. Garage has no concept of "primary" so any
healthy node with the data can be used as long as a quorum is reached for the metadata.
## Node health
Garage keeps a TCP session open to each node in the cluster and periodically pings them. If a connection
cannot be established, or a node fails to answer a number of pings, the target node is marked as failed.
Failed nodes are not used for quorum or other internal requests.
## Node preference
Garage prioritizes which nodes to query according to a few criteria:
- A node always prefers itself if it can answer the request
- Then the node prioritizes nodes in the same zone
- Finally the nodes with the lowest latency are prioritized
For further reading on the cluster structure look at the [gateway](@/documentation/cookbook/gateways.md)
and [cluster layout management](@/documentation/reference-manual/layout.md) pages.
@@ -1,6 +1,6 @@
+++
title = "S3 Compatibility status"
weight = 20
weight = 40
+++
## DISCLAIMER
+2 -2
View File
@@ -1,6 +1,6 @@
+++
title = "Design draft"
weight = 25
title = "Design draft (obsolete)"
weight = 50
+++
**WARNING: this documentation is a design draft which was written before Garage's actual implementation.
+2 -2
View File
@@ -1,6 +1,6 @@
+++
title = "Load balancing data"
weight = 10
title = "Load balancing data (obsolete)"
weight = 60
+++
**This is being yet improved in release 0.5. The working document has not been updated yet, it still only applies to Garage 0.2 through 0.4.**
+1 -1
View File
@@ -16,7 +16,7 @@ The migration steps are as follows:
1. Do `garage repair --all-nodes --yes tables` and `garage repair --all-nodes --yes blocks`,
check the logs and check that all data seems to be synced correctly between
nodes. If you have time, do additional checks (`scrub`, `block_refs`, etc.)
2. Disable api and web access. Garage does not support disabling
2. Disable API and web access. Garage does not support disabling
these endpoints but you can change the port number or stop your reverse
proxy for instance.
3. Check once again that your cluster is healty. Run again `garage repair --all-nodes --yes tables` which is quick.
@@ -0,0 +1,34 @@
+++
title = "Migrating from 0.7 to 0.8"
weight = 13
+++
**This guide explains how to migrate to 0.8 if you have an existing 0.7 cluster.
We don't recommend trying to migrate to 0.8 directly from 0.6 or older.**
**We make no guarantee that this migration will work perfectly:
back up all your data before attempting it!**
Garage v0.8 introduces new data tables that allow the counting of objects in buckets in order to implement bucket quotas.
A manual migration step is required to first count objects in Garage buckets and populate these tables with accurate data.
The migration steps are as follows:
1. Disable API and web access. Garage v0.7 does not support disabling
these endpoints but you can change the port number or stop your reverse proxy for instance.
2. Do `garage repair --all-nodes --yes tables` and `garage repair --all-nodes --yes blocks`,
check the logs and check that all data seems to be synced correctly between
nodes. If you have time, do additional checks (`scrub`, `block_refs`, etc.)
3. Check that queues are empty: run `garage stats` to query them or inspect metrics in the Grafana dashboard.
4. Turn off Garage v0.7
5. **Backup the metadata folder of all your nodes!** For instance, use the following command
if your metadata directory is `/var/lib/garage/meta`: `cd /var/lib/garage ; tar -acf meta-v0.7.tar.zst meta/`
6. Install Garage v0.8
7. **Before starting Garage v0.8**, run the offline migration step: `garage offline-repair --yes object_counters`.
This can take a while to run, depending on the number of objects stored in your cluster.
8. Turn on Garage v0.8
9. Do `garage repair --all-nodes --yes tables` and `garage repair --all-nodes --yes blocks`.
Wait for a full table sync to run.
10. Your upgraded cluster should be in a working state. Re-enable API and Web
access and check that everything went well.
11. Monitor your cluster in the next hours to see if it works well under your production load, report any issue.
+28 -28
View File
@@ -206,8 +206,8 @@ and responses need to be translated.
Query parameters:
| name | default value | meaning |
| - | - | - |
| name | default value | meaning |
|------------|---------------|----------------------------------|
| `sort_key` | **mandatory** | The sort key of the item to read |
Returns the item with specified partition key and sort key. Values can be
@@ -317,11 +317,11 @@ an HTTP 304 NOT MODIFIED is returned.
Query parameters:
| name | default value | meaning |
| - | - | - |
| `sort_key` | **mandatory** | The sort key of the item to read |
| `causality_token` | **mandatory** | The causality token of the last known value or set of values |
| `timeout` | 300 | The timeout before 304 NOT MODIFIED is returned if the value isn't updated |
| name | default value | meaning |
|-------------------|---------------|----------------------------------------------------------------------------|
| `sort_key` | **mandatory** | The sort key of the item to read |
| `causality_token` | **mandatory** | The causality token of the last known value or set of values |
| `timeout` | 300 | The timeout before 304 NOT MODIFIED is returned if the value isn't updated |
The timeout can be set to any number of seconds, with a maximum of 600 seconds (10 minutes).
@@ -346,7 +346,7 @@ myblobblahblahblah
Example response:
```
HTTP/1.1 200 OK
HTTP/1.1 204 No Content
```
**DeleteItem: `DELETE /<bucket>/<partition key>?sort_key=<sort_key>`**
@@ -382,13 +382,13 @@ as these values are asynchronously updated, and thus eventually consistent.
Query parameters:
| name | default value | meaning |
| - | - | - |
| `prefix` | `null` | Restrict listing to partition keys that start with this prefix |
| `start` | `null` | First partition key to list, in lexicographical order |
| `end` | `null` | Last partition key to list (excluded) |
| `limit` | `null` | Maximum number of partition keys to list |
| `reverse` | `false` | Iterate in reverse lexicographical order |
| name | default value | meaning |
|-----------|---------------|----------------------------------------------------------------|
| `prefix` | `null` | Restrict listing to partition keys that start with this prefix |
| `start` | `null` | First partition key to list, in lexicographical order |
| `end` | `null` | Last partition key to list (excluded) |
| `limit` | `null` | Maximum number of partition keys to list |
| `reverse` | `false` | Iterate in reverse lexicographical order |
The response consists in a JSON object that repeats the parameters of the query and gives the result (see below).
@@ -512,7 +512,7 @@ POST /my_bucket HTTP/1.1
Example response:
```
HTTP/1.1 200 OK
HTTP/1.1 204 NO CONTENT
```
@@ -525,17 +525,17 @@ The request body is a JSON list of searches, that each specify a range of
items to get (to get single items, set `singleItem` to `true`). A search is a
JSON struct with the following fields:
| name | default value | meaning |
| - | - | - |
| `partitionKey` | **mandatory** | The partition key in which to search |
| `prefix` | `null` | Restrict items to list to those whose sort keys start with this prefix |
| `start` | `null` | The sort key of the first item to read |
| `end` | `null` | The sort key of the last item to read (excluded) |
| `limit` | `null` | The maximum number of items to return |
| `reverse` | `false` | Iterate in reverse lexicographical order on sort keys |
| `singleItem` | `false` | Whether to return only the item with sort key `start` |
| `conflictsOnly` | `false` | Whether to return only items that have several concurrent values |
| `tombstones` | `false` | Whether or not to return tombstone lines to indicate the presence of old deleted items |
| name | default value | meaning |
|-----------------|---------------|----------------------------------------------------------------------------------------|
| `partitionKey` | **mandatory** | The partition key in which to search |
| `prefix` | `null` | Restrict items to list to those whose sort keys start with this prefix |
| `start` | `null` | The sort key of the first item to read |
| `end` | `null` | The sort key of the last item to read (excluded) |
| `limit` | `null` | The maximum number of items to return |
| `reverse` | `false` | Iterate in reverse lexicographical order on sort keys |
| `singleItem` | `false` | Whether to return only the item with sort key `start` |
| `conflictsOnly` | `false` | Whether to return only items that have several concurrent values |
| `tombstones` | `false` | Whether or not to return tombstone lines to indicate the presence of old deleted items |
For each of the searches, triplets are listed and returned separately. The
@@ -683,7 +683,7 @@ POST /my_bucket?delete HTTP/1.1
Example response:
```
```json
HTTP/1.1 200 OK
[
+8 -8
View File
@@ -3,20 +3,20 @@ rec {
* Fixed dependencies
*/
pkgsSrc = fetchTarball {
# As of 2021-10-04
url = "https://github.com/NixOS/nixpkgs/archive/b27d18a412b071f5d7991d1648cfe78ee7afe68a.tar.gz";
sha256 = "1xy9zpypqfxs5gcq5dcla4bfkhxmh5nzn9dyqkr03lqycm9wg5cr";
# As of 2022-10-13
url = "https://github.com/NixOS/nixpkgs/archive/a3073c49bc0163fea6a121c276f526837672b555.zip";
sha256 = "1bz632psfbpmicyzjb8b4265y50shylccvfm6ry6mgnv5hvz324s";
};
cargo2nixSrc = fetchGit {
# As of 2022-03-17
url = "https://github.com/superboum/cargo2nix";
ref = "dedup_propagate";
rev = "486675c67249e735dd7eb68e1b9feac9db102be7";
# As of 2022-10-18: two small patches over unstable branch, one for clippy and one to fix feature detection
url = "https://github.com/Alexis211/cargo2nix";
ref = "custom_unstable";
rev = "a7a61179b66054904ef6a195d8da736eaaa06c36";
};
/*
* Shared objects
*/
cargo2nix = import cargo2nixSrc;
cargo2nixOverlay = import "${cargo2nixSrc}/overlay";
cargo2nixOverlay = cargo2nix.overlays.default;
}
+85 -92
View File
@@ -1,9 +1,10 @@
{
system ? builtins.currentSystem,
target ? null,
target,
compiler ? "rustc",
release ? false,
git_version ? null,
features ? null,
}:
with import ./common.nix;
@@ -12,71 +13,41 @@ let
log = v: builtins.trace v v;
pkgs = import pkgsSrc {
inherit system;
${ if target == null then null else "crossSystem" } = { config = target; };
inherit system;
crossSystem = {
config = target;
isStatic = true;
};
overlays = [ cargo2nixOverlay ];
};
/*
Rust and Nix triples are not the same. Cargo2nix has a dedicated library
to convert Nix triples to Rust ones. We need this conversion as we want to
set later options linked to our (rust) target in a generic way. Not only
the triple terminology is different, but also the "roles" are named differently.
Nix uses a build/host/target terminology where Nix's "host" maps to Cargo's "target".
*/
rustTarget = log (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.hostPlatform);
/*
Cargo2nix is built for rustOverlay which installs Rust from Mozilla releases.
We want our own Rust to avoid incompatibilities, like we had with musl 1.2.0.
rustc was built with musl < 1.2.0 and nix shipped musl >= 1.2.0 which lead to compilation breakage.
This is fine for 64-bit platforms, but for 32-bit platforms, we need our own Rust
to avoid incompatibilities with time_t between different versions of musl
(>= 1.2.0 shipped by NixOS, < 1.2.0 with which rustc was built), which lead to compilation breakage.
So we want a Rust release that is bound to our Nix repository to avoid these problems.
See here for more info: https://musl.libc.org/time64.html
Because Cargo2nix does not support the Rust environment shipped by NixOS,
we emulate the structure of the Rust object created by rustOverlay.
In practise, rustOverlay ships rustc+cargo in a single derivation while
NixOS ships them in separate ones. We reunite them with symlinkJoin.
*/
rustChannel = {
rustc = pkgs.symlinkJoin {
name = "rust-channel";
paths = [
pkgs.rustPlatform.rust.cargo
pkgs.rustPlatform.rust.rustc
];
*/
toolchainOptions =
if target == "x86_64-unknown-linux-musl" || target == "aarch64-unknown-linux-musl" then {
rustVersion = "1.63.0";
extraRustComponents = [ "clippy" ];
} else {
rustToolchain = pkgs.symlinkJoin {
name = "rust-static-toolchain-${target}";
paths = [
pkgs.rustPlatform.rust.cargo
pkgs.rustPlatform.rust.rustc
# clippy not needed, it only runs on amd64
];
};
};
clippy = pkgs.symlinkJoin {
name = "clippy-channel";
paths = [
pkgs.rustPlatform.rust.cargo
pkgs.rustPlatform.rust.rustc
pkgs.clippy
];
};
}.${compiler};
clippyBuilder = pkgs.writeScriptBin "clippy" ''
#!${pkgs.stdenv.shell}
. ${cargo2nixSrc + "/overlay/utils.sh"}
isBuildScript=
args=("$@")
for i in "''${!args[@]}"; do
if [ "xmetadata=" = "x''${args[$i]::9}" ]; then
args[$i]=metadata=$NIX_RUST_METADATA
elif [ "x--crate-name" = "x''${args[$i]}" ] && [ "xbuild_script_" = "x''${args[$i+1]::13}" ]; then
isBuildScript=1
fi
done
if [ "$isBuildScript" ]; then
args+=($NIX_RUST_BUILD_LINK_FLAGS)
else
args+=($NIX_RUST_LINK_FLAGS)
fi
touch invoke.log
echo "''${args[@]}" >>invoke.log
exec ${rustChannel}/bin/clippy-driver --deny warnings "''${args[@]}"
'';
buildEnv = (drv: {
rustc = drv.setBuildEnv;
@@ -84,9 +55,10 @@ let
${drv.setBuildEnv or "" }
echo
echo --- BUILDING WITH CLIPPY ---
echo
echo
export RUSTC=${clippyBuilder}/bin/clippy
export NIX_RUST_BUILD_FLAGS="''${NIX_RUST_BUILD_FLAGS} --deny warnings"
export RUSTC="''${CLIPPY_DRIVER}"
'';
}.${compiler});
@@ -97,7 +69,7 @@ let
You can have a complete list of the available options by looking at the overriden object, mkcrate:
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/mkcrate.nix
*/
overrides = pkgs.rustBuilder.overrides.all ++ [
packageOverrides = pkgs: pkgs.rustBuilder.overrides.all ++ [
/*
[1] We add some logic to compile our crates with clippy, it provides us many additional lints
@@ -113,22 +85,12 @@ let
As we do not want to consider the .git folder as part of the input source,
we ask the user (the CI often) to pass the value to Nix.
[4] We ship some parts of the code disabled by default by putting them behind a flag.
It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.).
But we want to ship these additional features when we release Garage.
In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds.
Currently, the only feature of Garage is kubernetes-discovery from the garage_rpc crate.
[4] We don't want libsodium-sys and zstd-sys to try to use pkgconfig to build against a system library.
However the features to do so get activated for some reason (due to a bug in cargo2nix?),
so disable them manually here.
*/
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage";
overrideAttrs = drv: {
/* [1] */ setBuildEnv = (buildEnv drv);
/* [2] */ hardeningDisable = [ "pie" ];
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_rpc";
overrideAttrs = drv:
(if git_version != null then {
/* [3] */ preConfigure = ''
@@ -136,14 +98,18 @@ let
export GIT_VERSION="${git_version}"
'';
} else {})
// {
/* [1] */ setBuildEnv = (buildEnv drv);
};
overrideArgs = old: {
/* [4] */ features = if release then [ "kubernetes-discovery" ] else [];
//
{
/* [1] */ setBuildEnv = (buildEnv drv);
/* [2] */ hardeningDisable = [ "pie" ];
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_rpc";
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_db";
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
@@ -183,8 +149,43 @@ let
name = "k2v-client";
overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); };
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "libsodium-sys";
overrideArgs = old: {
features = [ ]; /* [4] */
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "zstd-sys";
overrideArgs = old: {
features = [ ]; /* [4] */
};
})
];
/*
We ship some parts of the code disabled by default by putting them behind a flag.
It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.).
But we want to ship these additional features when we release Garage.
In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds.
*/
rootFeatures = if features != null then features else
([
"garage/bundled-libs"
"garage/sled"
"garage/k2v"
] ++ (if release then [
"garage/consul-discovery"
"garage/kubernetes-discovery"
"garage/metrics"
"garage/telemetry-otlp"
"garage/lmdb"
"garage/sqlite"
] else []));
packageFun = import ../Cargo.nix;
/*
@@ -205,23 +206,15 @@ let
"x86_64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static-pie" ];
};
in
/*
The following definition is not elegant as we use a low level function of Cargo2nix
that enables us to pass our custom rustChannel object. We need this low level definition
to pass Nix's Rust toolchains instead of Mozilla's one.
NixOS and Rust/Cargo triples do not match for ARM, fix it here.
*/
rustTarget = if target == "armv6l-unknown-linux-musleabihf"
then "arm-unknown-linux-musleabihf"
else target;
target is mandatory but must be kept to null to allow cargo2nix to set it to the appropriate value
for each crate.
*/
pkgs.rustBuilder.makePackageSet {
inherit packageFun rustChannel release codegenOpts;
packageOverrides = overrides;
target = null;
buildRustPackages = pkgs.buildPackages.rustBuilder.makePackageSet {
inherit rustChannel packageFun codegenOpts;
packageOverrides = overrides;
target = null;
};
}
in
pkgs.rustBuilder.makePackageSet ({
inherit release packageFun packageOverrides codegenOpts rootFeatures;
target = rustTarget;
} // toolchainOptions)
+23
View File
@@ -0,0 +1,23 @@
pkgs:
pkgs.buildGoModule rec {
pname = "manifest-tool";
version = "2.0.5";
src = pkgs.fetchFromGitHub {
owner = "estesp";
repo = "manifest-tool";
rev = "v${version}";
sha256 = "hjCGKnE0yrlnF/VIzOwcDzmQX3Wft+21KCny/opqdLg=";
} + "/v2";
vendorSha256 = null;
checkPhase = "true";
meta = with pkgs.lib; {
description = "Command line tool to create and query container image manifest list/indexes";
homepage = "https://github.com/estesp/manifest-tool";
license = licenses.asl20;
platforms = platforms.linux;
};
}
+9 -3
View File
@@ -6,19 +6,24 @@ with import ./common.nix;
let
platforms = [
"x86_64-unknown-linux-musl"
#"x86_64-unknown-linux-musl"
"i686-unknown-linux-musl"
"aarch64-unknown-linux-musl"
#"aarch64-unknown-linux-musl"
"armv6l-unknown-linux-musleabihf"
];
pkgsList = builtins.map (target: import pkgsSrc {
inherit system;
crossSystem = { config = target; };
crossSystem = {
config = target;
isStatic = true;
};
overlays = [ cargo2nixOverlay ];
}) platforms;
pkgsHost = import pkgsSrc {};
lib = pkgsHost.lib;
kaniko = (import ./kaniko.nix) pkgsHost;
winscp = (import ./winscp.nix) pkgsHost;
manifestTool = (import ./manifest-tool.nix) pkgsHost;
in
lib.flatten (builtins.map (pkgs: [
pkgs.rustPlatform.rust.rustc
@@ -27,5 +32,6 @@ in
]) pkgsList) ++ [
kaniko
winscp
manifestTool
]
+1 -1
View File
@@ -11,7 +11,7 @@ PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:${NIX_RELEASE}:$PATH"
FANCYCOLORS=("41m" "42m" "44m" "45m" "100m" "104m")
export RUST_BACKTRACE=1
export RUST_LOG=garage=info,garage_api=debug
export RUST_LOG=garage=info,garage_api=debug,netapp=trace
MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m"
WHICH_GARAGE=$(which garage || exit 1)
+3
View File
@@ -0,0 +1,3 @@
# Garage helm3 chart
Documentation is located [here](/doc/book/cookbook/kubernetes.md).
+23
View File
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
+24
View File
@@ -0,0 +1,24 @@
apiVersion: v2
name: garage
description: S3-compatible object store for small self-hosted geo-distributed deployments
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v0.7.2.1"
+88
View File
@@ -0,0 +1,88 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "garage.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "garage.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create the name of the rpc secret
*/}}
{{- define "garage.rpcSecretName" -}}
{{- printf "%s-rpc-secret" (include "garage.fullname" .) -}}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "garage.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "garage.labels" -}}
helm.sh/chart: {{ include "garage.chart" . }}
{{ include "garage.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "garage.selectorLabels" -}}
app.kubernetes.io/name: {{ include "garage.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "garage.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "garage.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Returns given number of random Hex characters.
In practice, it generates up to 100 randAlphaNum strings
that are filtered from non-hex characters and augmented
to the resulting string that is finally trimmed down.
*/}}
{{- define "jupyterhub.randHex" -}}
{{- $result := "" }}
{{- range $i := until 100 }}
{{- if lt (len $result) . }}
{{- $rand_list := randAlphaNum . | splitList "" -}}
{{- $reduced_list := without $rand_list "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z" "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" }}
{{- $rand_string := join "" $reduced_list }}
{{- $result = print $result $rand_string -}}
{{- end }}
{{- end }}
{{- $result | trunc . }}
{{- end }}
@@ -0,0 +1,28 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: manage-crds-{{ .Release.Namespace }}-{{ .Release.Name }}
labels:
{{- include "garage.labels" . | nindent 4 }}
rules:
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch", "create", "patch"]
- apiGroups: ["deuxfleurs.fr"]
resources: ["garagenodes"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: allow-crds-for-{{ .Release.Namespace }}-{{ .Release.Name }}
labels:
{{- include "garage.labels" . | nindent 4 }}
subjects:
- kind: ServiceAccount
name: {{ include "garage.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
roleRef:
kind: ClusterRole
name: manage-crds-{{ .Release.Namespace }}-{{ .Release.Name }}
apiGroup: rbac.authorization.k8s.io
@@ -0,0 +1,30 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "garage.fullname" . }}-config
data:
garage.toml: |-
metadata_dir = "{{ .Values.garage.metadataDir }}"
data_dir = "{{ .Values.garage.dataDir }}"
replication_mode = "{{ .Values.garage.replicationMode }}"
rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}"
# rpc_secret will be populated by the init container from a k8s secret object
rpc_secret = "__RPC_SECRET_REPLACE__"
bootstrap_peers = {{ .Values.garage.bootstrapPeers }}
kubernetes_namespace = "{{ .Release.Namespace }}"
kubernetes_service_name = "{{ include "garage.fullname" . }}"
kubernetes_skip_crd = {{ .Values.garage.kubernetesSkipCrd }}
[s3_api]
s3_region = "{{ .Values.garage.s3.api.region }}"
api_bind_addr = "[::]:3900"
root_domain = "{{ .Values.garage.s3.api.rootDomain }}"
[s3_web]
bind_addr = "[::]:3902"
root_domain = "{{ .Values.garage.s3.web.rootDomain }}"
index = "{{ .Values.garage.s3.web.index }}"
+123
View File
@@ -0,0 +1,123 @@
{{- if .Values.ingress.s3.api.enabled -}}
{{- $fullName := include "garage.fullname" . -}}
{{- $svcPort := .Values.service.s3.api.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.s3.api.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.s3.api.annotations "kubernetes.io/ingress.class" .Values.ingress.s3.api.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}-s3-api
labels:
{{- include "garage.labels" . | nindent 4 }}
{{- with .Values.ingress.s3.api.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.s3.api.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.s3.api.className }}
{{- end }}
{{- if .Values.ingress.s3.api.tls }}
tls:
{{- range .Values.ingress.s3.api.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.s3.api.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
---
{{- if .Values.ingress.s3.web.enabled -}}
{{- $fullName := include "garage.fullname" . -}}
{{- $svcPort := .Values.service.s3.web.port -}}
{{- if and .Values.ingress.s3.web.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.s3.web.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.s3.web.annotations "kubernetes.io/ingress.class" .Values.ingress.s3.web.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}-s3-web
labels:
{{- include "garage.labels" . | nindent 4 }}
{{- with .Values.ingress.s3.web.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.s3.web.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.s3.web.className }}
{{- end }}
{{- if .Values.ingress.s3.web.tls }}
tls:
{{- range .Values.ingress.s3.web.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.s3.web.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "garage.rpcSecretName" . }}
labels:
{{- include "garage.labels" . | nindent 4 }}
type: Opaque
data:
{{/* retrieve the secret data using lookup function and when not exists, return an empty dictionary / map as result */}}
{{- $prevSecret := (lookup "v1" "Secret" .Release.Namespace (include "garage.rpcSecretName" .)) | default dict }}
{{- $prevSecretData := $prevSecret.data | default dict }}
{{- $prevRpcSecret := $prevSecretData.rpcSecret | default "" | b64dec }}
{{/* Priority is: 1. from values, 2. previous value, 3. generate random */}}
rpcSecret: {{ .Values.garage.rpcSecret | default $prevRpcSecret | default (include "jupyterhub.randHex" 64) | b64enc | quote }}
+19
View File
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "garage.fullname" . }}
labels:
{{- include "garage.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.s3.api.port }}
targetPort: 3900
protocol: TCP
name: s3-api
- port: {{ .Values.service.s3.web.port }}
targetPort: 3902
protocol: TCP
name: s3-web
selector:
{{- include "garage.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "garage.serviceAccountName" . }}
labels:
{{- include "garage.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,116 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "garage.fullname" . }}
labels:
{{- include "garage.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "garage.selectorLabels" . | nindent 6 }}
serviceName: {{ include "garage.fullname" . }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "garage.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "garage.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
initContainers:
# Copies garage.toml from configmap to temporary etc volume and replaces RPC secret placeholder
- name: {{ .Chart.Name }}-init
image: busybox:1.28
command: ["sh", "-c", "sed \"s/__RPC_SECRET_REPLACE__/$RPC_SECRET/\" /mnt/garage.toml > /mnt/etc/garage.toml"]
env:
- name: RPC_SECRET
valueFrom:
secretKeyRef:
name: {{ include "garage.rpcSecretName" . }}
key: rpcSecret
volumeMounts:
- name: configmap
mountPath: /mnt/garage.toml
subPath: garage.toml
- name: etc
mountPath: /mnt/etc
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 3900
name: s3-api
- containerPort: 3902
name: web-api
volumeMounts:
- name: meta
mountPath: /mnt/meta
- name: data
mountPath: /mnt/data
- name: etc
mountPath: /etc/garage.toml
subPath: garage.toml
# TODO
# livenessProbe:
# httpGet:
# path: /
# port: 3900
# readinessProbe:
# httpGet:
# path: /
# port: 3900
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: configmap
configMap:
name: {{ include "garage.fullname" . }}-config
- name: etc
emptyDir: {}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: meta
spec:
accessModes: [ "ReadWriteOnce" ]
{{- if hasKey .Values.persistence.meta "storageClass" }}
storageClassName: {{ .Values.persistence.meta.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.meta.size | quote }}
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
{{- if hasKey .Values.persistence.data "storageClass" }}
storageClassName: {{ .Values.persistence.data.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.data.size | quote }}
{{- end }}
+142
View File
@@ -0,0 +1,142 @@
# Default values for garage.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# Garage configuration. These values go to garage.toml
garage:
metadataDir: "/mnt/meta"
dataDir: "/mnt/data"
# Default to 3 replicas, see the replication_mode section at
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/
replicationMode: "3"
rpcBindAddr: "[::]:3901"
# If not given, a random secret will be generated and stored in a Secret object
rpcSecret: ""
# This is not required if you use the integrated kubernetes discovery
bootstrapPeers: []
kubernetesSkipCrd: false
s3:
api:
region: "garage"
rootDomain: ".s3.garage.tld"
web:
rootDomain: ".web.garage.tld"
index: "index.html"
# Data persistence
persistence:
enabled: true
meta:
# storageClass: "fast-storage-class"
size: 100Mi
data:
# storageClass: "slow-storage-class"
size: 100Mi
# Number of StatefulSet replicas/garage nodes to start
replicaCount: 3
image:
repository: dxflrs/amd64_garage
# please prefer using the chart version and not this tag
tag: ""
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext:
# The default security context is heavily restricted
# feel free to tune it to your requirements
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
service:
# You can rely on any service to expose your cluster
# - ClusterIP (+ Ingress)
# - NodePort (+ Ingress)
# - LoadBalancer
type: ClusterIP
s3:
api:
port: 3900
web:
port: 3902
# NOTE: the admin API is excluded for now as it is not consistent across nodes
ingress:
s3:
api:
enabled: true
# Rely either on the className or the annotation below but not both
# replace "nginx" by an Ingress controller
# you can find examples here https://kubernetes.io/docs/concepts/services-networking/ingress-controllers
className: "nginx"
annotations:
# kubernetes.io/ingress.class: "nginx"
# kubernetes.io/tls-acme: "true"
hosts:
- host: "s3.garage.tld" # garage S3 API endpoint
paths:
- path: /
pathType: Prefix
- host: "*.s3.garage.tld" # garage S3 API endpoint, DNS style bucket access
paths:
- path: /
pathType: Prefix
tls: []
# - secretName: my-garage-cluster-tls
# hosts:
# - kubernetes.docker.internal
web:
enabled: true
className: "nginx"
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: "*.web.garage.tld" # wildcard website access with bucket name prefix
paths:
- path: /
pathType: Prefix
- host: "mywebpage.example.com" # specific bucket access with FQDN bucket
paths:
- path: /
pathType: Prefix
tls: []
# - secretName: my-garage-cluster-tls
# hosts:
# - kubernetes.docker.internal
resources: {}
# The following are indicative for a small-size deployement, for anything serious double them.
# limits:
# cpu: 100m
# memory: 1024Mi
# requests:
# cpu: 100m
# memory: 512Mi
nodeSelector: {}
tolerations: []
affinity: {}
+1 -1
View File
@@ -8,7 +8,7 @@ SCRIPT_FOLDER="`dirname \"$0\"`"
REPO_FOLDER="${SCRIPT_FOLDER}/../"
GARAGE_DEBUG="${REPO_FOLDER}/target/debug/"
GARAGE_RELEASE="${REPO_FOLDER}/target/release/"
NIX_RELEASE="${REPO_FOLDER}/result/bin/"
NIX_RELEASE="${REPO_FOLDER}/result/bin/:${REPO_FOLDER}/result-bin/bin/"
PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:${NIX_RELEASE}:$PATH"
CMDOUT=/tmp/garage.cmd.tmp
+52 -11
View File
@@ -10,24 +10,15 @@ let
overlays = [ cargo2nixOverlay ];
};
kaniko = (import ./nix/kaniko.nix) pkgs;
manifest-tool = (import ./nix/manifest-tool.nix) pkgs;
winscp = (import ./nix/winscp.nix) pkgs;
in
{
/* --- Rust Shell ---
* Use it to compile Garage
*/
rust = pkgs.mkShell {
shellHook = ''
function refresh_toolchain {
nix copy \
--to 's3://nix?endpoint=garage.deuxfleurs.fr&region=garage&secret-key=/etc/nix/signing-key.sec' \
$(nix-store -qR \
$(nix-build --quiet --no-build-output --no-out-link nix/toolchain.nix))
}
'';
nativeBuildInputs = [
#pkgs.rustPlatform.rust.rustc
pkgs.rustPlatform.rust.cargo
@@ -66,12 +57,33 @@ function refresh_toolchain {
*/
release = pkgs.mkShell {
shellHook = ''
function refresh_toolchain {
pass show deuxfleurs/nix_priv_key > /tmp/nix-signing-key.sec
nix copy \
--to 's3://nix?endpoint=garage.deuxfleurs.fr&region=garage&secret-key=/tmp/nix-signing-key.sec' \
$(nix-store -qR \
$(nix-build --no-build-output --no-out-link nix/toolchain.nix))
rm /tmp/nix-signing-key.sec
}
function refresh_cache {
pass show deuxfleurs/nix_priv_key > /tmp/nix-signing-key.sec
for attr in clippy.amd64 test.amd64 pkgs.{amd64,i386,arm,arm64}.{debug,release}; do
echo "Updating cache for ''${attr}"
derivation=$(nix-instantiate --attr ''${attr})
nix copy \
--to 's3://nix?endpoint=garage.deuxfleurs.fr&region=garage&secret-key=/tmp/nix-signing-key.sec' \
$(nix-store -qR ''${derivation%\!bin})
done
rm /tmp/nix-signing-key.sec
}
function to_s3 {
aws \
--endpoint-url https://garage.deuxfleurs.fr \
--region garage \
s3 cp \
./result/bin/garage \
./result-bin/bin/garage \
s3://garagehq.deuxfleurs.fr/_releases/''${DRONE_TAG:-$DRONE_COMMIT}/''${TARGET}/garage
}
@@ -84,6 +96,34 @@ function to_docker {
--verbosity=debug
}
function multiarch_docker {
manifest-tool push from-spec <(cat <<EOF
image: dxflrs/garage:''${CONTAINER_TAG}
manifests:
-
image: dxflrs/arm64_garage:''${CONTAINER_TAG}
platform:
architecture: arm64
os: linux
-
image: dxflrs/amd64_garage:''${CONTAINER_TAG}
platform:
architecture: amd64
os: linux
-
image: dxflrs/386_garage:''${CONTAINER_TAG}
platform:
architecture: 386
os: linux
-
image: dxflrs/arm_garage:''${CONTAINER_TAG}
platform:
architecture: arm
os: linux
EOF
)
}
function refresh_index {
aws \
--endpoint-url https://garage.deuxfleurs.fr \
@@ -113,6 +153,7 @@ function refresh_index {
nativeBuildInputs = [
pkgs.awscli2
kaniko
manifest-tool
];
};
}
+15 -14
View File
@@ -1,6 +1,6 @@
[package]
name = "garage_api"
version = "0.7.0"
version = "0.8.0"
authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018"
license = "AGPL-3.0"
@@ -14,30 +14,31 @@ path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
garage_model = { version = "0.7.0", path = "../model" }
garage_table = { version = "0.7.0", path = "../table" }
garage_block = { version = "0.7.0", path = "../block" }
garage_util = { version = "0.7.0", path = "../util" }
garage_rpc = { version = "0.7.0", path = "../rpc" }
garage_model = { version = "0.8.0", path = "../model" }
garage_table = { version = "0.8.0", path = "../table" }
garage_block = { version = "0.8.0", path = "../block" }
garage_util = { version = "0.8.0", path = "../util" }
garage_rpc = { version = "0.8.0", path = "../rpc" }
async-trait = "0.1.7"
base64 = "0.13"
bytes = "1.0"
chrono = "0.4"
crypto-mac = "0.10"
crypto-common = "0.1"
err-derive = "0.3"
hex = "0.4"
hmac = "0.10"
hmac = "0.12"
idna = "0.2"
tracing = "0.1.30"
md-5 = "0.9"
md-5 = "0.10"
nom = "7.1"
sha2 = "0.9"
sha2 = "0.10"
futures = "0.3"
futures-util = "0.3"
pin-project = "1.0"
pin-project = "1.0.11"
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
tokio-stream = "0.1"
form_urlencoded = "1.0.0"
http = "0.2"
@@ -54,9 +55,9 @@ quick-xml = { version = "0.21", features = [ "serialize" ] }
url = "2.1"
opentelemetry = "0.17"
opentelemetry-prometheus = "0.10"
opentelemetry-otlp = "0.10"
prometheus = "0.13"
opentelemetry-prometheus = { version = "0.10", optional = true }
prometheus = { version = "0.13", optional = true }
[features]
k2v = [ "garage_util/k2v", "garage_model/k2v" ]
metrics = [ "opentelemetry-prometheus", "prometheus" ]
+45 -30
View File
@@ -1,15 +1,17 @@
use std::net::SocketAddr;
use std::sync::Arc;
use async_trait::async_trait;
use futures::future::Future;
use http::header::{
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW, CONTENT_TYPE,
};
use hyper::{Body, Request, Response};
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
use hyper::{Body, Request, Response, StatusCode};
use opentelemetry::trace::{SpanRef, Tracer};
use opentelemetry::trace::SpanRef;
#[cfg(feature = "metrics")]
use opentelemetry_prometheus::PrometheusExporter;
#[cfg(feature = "metrics")]
use prometheus::{Encoder, TextEncoder};
use garage_model::garage::Garage;
@@ -25,14 +27,17 @@ use crate::admin::router::{Authorization, Endpoint};
pub struct AdminApiServer {
garage: Arc<Garage>,
#[cfg(feature = "metrics")]
exporter: PrometheusExporter,
metrics_token: Option<String>,
admin_token: Option<String>,
}
impl AdminApiServer {
pub fn new(garage: Arc<Garage>) -> Self {
let exporter = opentelemetry_prometheus::exporter().init();
pub fn new(
garage: Arc<Garage>,
#[cfg(feature = "metrics")] exporter: PrometheusExporter,
) -> Self {
let cfg = &garage.config.admin;
let metrics_token = cfg
.metrics_token
@@ -44,26 +49,27 @@ impl AdminApiServer {
.map(|tok| format!("Bearer {}", tok));
Self {
garage,
#[cfg(feature = "metrics")]
exporter,
metrics_token,
admin_token,
}
}
pub async fn run(self, shutdown_signal: impl Future<Output = ()>) -> Result<(), GarageError> {
if let Some(bind_addr) = self.garage.config.admin.api_bind_addr {
let region = self.garage.config.s3_api.s3_region.clone();
ApiServer::new(region, self)
.run_server(bind_addr, shutdown_signal)
.await
} else {
Ok(())
}
pub async fn run(
self,
bind_addr: SocketAddr,
shutdown_signal: impl Future<Output = ()>,
) -> Result<(), GarageError> {
let region = self.garage.config.s3_api.s3_region.clone();
ApiServer::new(region, self)
.run_server(bind_addr, shutdown_signal)
.await
}
fn handle_options(&self, _req: &Request<Body>) -> Result<Response<Body>, Error> {
Ok(Response::builder()
.status(204)
.status(StatusCode::NO_CONTENT)
.header(ALLOW, "OPTIONS, GET, POST")
.header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST")
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
@@ -71,22 +77,31 @@ impl AdminApiServer {
}
fn handle_metrics(&self) -> Result<Response<Body>, Error> {
let mut buffer = vec![];
let encoder = TextEncoder::new();
#[cfg(feature = "metrics")]
{
use opentelemetry::trace::Tracer;
let tracer = opentelemetry::global::tracer("garage");
let metric_families = tracer.in_span("admin/gather_metrics", |_| {
self.exporter.registry().gather()
});
let mut buffer = vec![];
let encoder = TextEncoder::new();
encoder
.encode(&metric_families, &mut buffer)
.ok_or_internal_error("Could not serialize metrics")?;
let tracer = opentelemetry::global::tracer("garage");
let metric_families = tracer.in_span("admin/gather_metrics", |_| {
self.exporter.registry().gather()
});
Ok(Response::builder()
.status(200)
.header(CONTENT_TYPE, encoder.format_type())
.body(Body::from(buffer))?)
encoder
.encode(&metric_families, &mut buffer)
.ok_or_internal_error("Could not serialize metrics")?;
Ok(Response::builder()
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, encoder.format_type())
.body(Body::from(buffer))?)
}
#[cfg(not(feature = "metrics"))]
Err(Error::bad_request(
"Garage was built without the metrics feature".to_string(),
))
}
}
+6 -4
View File
@@ -18,7 +18,8 @@ use crate::helpers::{json_ok_response, parse_json_body};
pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
let res = GetClusterStatusResponse {
node: hex::encode(garage.system.id),
garage_version: garage.system.garage_version(),
garage_version: garage_util::version::garage_version(),
garage_features: garage_util::version::garage_features(),
db_engine: garage.db.engine(),
known_nodes: garage
.system
@@ -99,6 +100,7 @@ fn get_cluster_layout(garage: &Arc<Garage>) -> GetClusterLayoutResponse {
struct GetClusterStatusResponse {
node: String,
garage_version: &'static str,
garage_features: Option<&'static [&'static str]>,
db_engine: String,
known_nodes: HashMap<String, KnownNodeResp>,
layout: GetClusterLayoutResponse,
@@ -149,7 +151,7 @@ pub async fn handle_update_cluster_layout(
garage.system.update_cluster_layout(&layout).await?;
Ok(Response::builder()
.status(StatusCode::OK)
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
@@ -164,7 +166,7 @@ pub async fn handle_apply_cluster_layout(
garage.system.update_cluster_layout(&layout).await?;
Ok(Response::builder()
.status(StatusCode::OK)
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
@@ -179,7 +181,7 @@ pub async fn handle_revert_cluster_layout(
garage.system.update_cluster_layout(&layout).await?;
Ok(Response::builder()
.status(StatusCode::OK)
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
+5 -1
View File
@@ -174,7 +174,11 @@ impl<A: ApiHandler> ApiServer<A> {
let current_context = Context::current();
let current_span = current_context.span();
current_span.update_name::<String>(format!("S3 API {}", endpoint.name()));
current_span.update_name::<String>(format!(
"{} API {}",
A::API_NAME_DISPLAY,
endpoint.name()
));
current_span.set_attribute(KeyValue::new("endpoint", endpoint.name()));
endpoint.add_span_attributes(current_span);
+4 -10
View File
@@ -1,3 +1,4 @@
use std::net::SocketAddr;
use std::sync::Arc;
use async_trait::async_trait;
@@ -36,20 +37,13 @@ pub(crate) struct K2VApiEndpoint {
impl K2VApiServer {
pub async fn run(
garage: Arc<Garage>,
bind_addr: SocketAddr,
s3_region: String,
shutdown_signal: impl Future<Output = ()>,
) -> Result<(), GarageError> {
if let Some(cfg) = &garage.config.k2v_api {
let bind_addr = cfg.api_bind_addr;
ApiServer::new(
garage.config.s3_api.s3_region.clone(),
K2VApiServer { garage },
)
ApiServer::new(s3_region, K2VApiServer { garage })
.run_server(bind_addr, shutdown_signal)
.await
} else {
Ok(())
}
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ pub async fn handle_insert_batch(
garage.k2v.rpc.insert_batch(bucket_id, items2).await?;
Ok(Response::builder()
.status(StatusCode::OK)
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
+1 -1
View File
@@ -153,7 +153,7 @@ pub async fn handle_insert_item(
.await?;
Ok(Response::builder()
.status(StatusCode::OK)
.status(StatusCode::NO_CONTENT)
.body(Body::empty())?)
}
+6 -8
View File
@@ -1,3 +1,4 @@
use std::net::SocketAddr;
use std::sync::Arc;
use async_trait::async_trait;
@@ -43,16 +44,13 @@ pub(crate) struct S3ApiEndpoint {
impl S3ApiServer {
pub async fn run(
garage: Arc<Garage>,
addr: SocketAddr,
s3_region: String,
shutdown_signal: impl Future<Output = ()>,
) -> Result<(), GarageError> {
let addr = garage.config.s3_api.api_bind_addr;
ApiServer::new(
garage.config.s3_api.s3_region.clone(),
S3ApiServer { garage },
)
.run_server(addr, shutdown_signal)
.await
ApiServer::new(s3_region, S3ApiServer { garage })
.run_server(addr, shutdown_signal)
.await
}
async fn handle_request_without_bucket(
-1
View File
@@ -295,7 +295,6 @@ fn parse_create_bucket_xml(xml_bytes: &[u8]) -> Option<Option<String>> {
let mut ret = None;
for item in cbc.children() {
println!("{:?}", item);
if item.has_tag_name("LocationConstraint") {
if ret != None {
return None;
+18 -11
View File
@@ -5,9 +5,12 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use futures::{stream, stream::Stream, StreamExt, TryFutureExt};
use md5::{Digest as Md5Digest, Md5};
use bytes::Bytes;
use hyper::{Body, Request, Response};
use serde::Serialize;
use garage_rpc::netapp::bytes_buf::BytesBuf;
use garage_rpc::rpc_helper::OrderTag;
use garage_table::*;
use garage_util::data::*;
use garage_util::time::*;
@@ -305,13 +308,18 @@ pub async fn handle_upload_part_copy(
// if and only if the block returned is a block that already existed
// in the Garage data store (thus we don't need to save it again).
let garage2 = garage.clone();
let order_stream = OrderTag::stream();
let source_blocks = stream::iter(blocks_to_copy)
.flat_map(|(block_hash, range_to_copy)| {
.enumerate()
.flat_map(|(i, (block_hash, range_to_copy))| {
let garage3 = garage2.clone();
stream::once(async move {
let data = garage3.block_manager.rpc_get_block(&block_hash).await?;
let data = garage3
.block_manager
.rpc_get_block(&block_hash, Some(order_stream.order(i as u64)))
.await?;
match range_to_copy {
Some(r) => Ok((data[r].to_vec(), None)),
Some(r) => Ok((data.slice(r), None)),
None => Ok((data, Some(block_hash))),
}
})
@@ -553,13 +561,13 @@ impl CopyPreconditionHeaders {
}
}
type BlockStreamItemOk = (Vec<u8>, Option<Hash>);
type BlockStreamItemOk = (Bytes, Option<Hash>);
type BlockStreamItem = Result<BlockStreamItemOk, garage_util::error::Error>;
struct Defragmenter<S: Stream<Item = BlockStreamItem>> {
block_size: usize,
block_stream: Pin<Box<stream::Peekable<S>>>,
buffer: Vec<u8>,
buffer: BytesBuf,
hash: Option<Hash>,
}
@@ -568,7 +576,7 @@ impl<S: Stream<Item = BlockStreamItem>> Defragmenter<S> {
Self {
block_size,
block_stream,
buffer: vec![],
buffer: BytesBuf::new(),
hash: None,
}
}
@@ -586,7 +594,7 @@ impl<S: Stream<Item = BlockStreamItem>> Defragmenter<S> {
if self.buffer.is_empty() {
let (next_block, next_block_hash) = self.block_stream.next().await.unwrap()?;
self.buffer = next_block;
self.buffer.extend(next_block);
self.hash = next_block_hash;
} else if self.buffer.len() + peeked_next_block.len() > self.block_size {
break;
@@ -597,11 +605,11 @@ impl<S: Stream<Item = BlockStreamItem>> Defragmenter<S> {
}
}
Ok((std::mem::take(&mut self.buffer), self.hash.take()))
Ok((self.buffer.take_all(), self.hash.take()))
}
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct CopyObjectResult {
#[serde(rename = "LastModified")]
pub last_modified: s3_xml::Value,
@@ -609,7 +617,7 @@ pub struct CopyObjectResult {
pub etag: s3_xml::Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct CopyPartResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
@@ -654,7 +662,6 @@ mod tests {
last_modified: s3_xml::Value("2011-04-11T20:34:56.000Z".into()),
etag: s3_xml::Value("\"9b2cf535f27731c974343645a3985328\"".into()),
};
println!("{}", to_xml_with_header(&v)?);
assert_eq!(to_xml_with_header(&v)?, expected_retval);
+7 -8
View File
@@ -64,14 +64,13 @@ pub async fn handle_delete(
bucket_id: Uuid,
key: &str,
) -> Result<Response<Body>, Error> {
let (_deleted_version, delete_marker_version) =
handle_delete_internal(&garage, bucket_id, key).await?;
Ok(Response::builder()
.header("x-amz-version-id", hex::encode(delete_marker_version))
.status(StatusCode::NO_CONTENT)
.body(Body::from(vec![]))
.unwrap())
match handle_delete_internal(&garage, bucket_id, key).await {
Ok(_) | Err(Error::NoSuchKey) => Ok(Response::builder()
.status(StatusCode::NO_CONTENT)
.body(Body::from(vec![]))
.unwrap()),
Err(e) => Err(e),
}
}
pub async fn handle_delete_objects(
+112 -50
View File
@@ -2,16 +2,19 @@
use std::sync::Arc;
use std::time::{Duration, UNIX_EPOCH};
use futures::stream::*;
use futures::future;
use futures::stream::{self, StreamExt};
use http::header::{
ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE,
IF_NONE_MATCH, LAST_MODIFIED, RANGE,
};
use hyper::body::Bytes;
use hyper::{Body, Request, Response, StatusCode};
use tokio::sync::mpsc;
use garage_rpc::rpc_helper::{netapp::stream::ByteStream, OrderTag};
use garage_table::EmptyKey;
use garage_util::data::*;
use garage_util::error::OkOrMessage;
use garage_model::garage::Garage;
use garage_model::s3::object_table::*;
@@ -242,36 +245,56 @@ pub async fn handle_get(
Ok(resp_builder.body(body)?)
}
ObjectVersionData::FirstBlock(_, first_block_hash) => {
let read_first_block = garage.block_manager.rpc_get_block(first_block_hash);
let get_next_blocks = garage.version_table.get(&last_v.uuid, &EmptyKey);
let (tx, rx) = mpsc::channel(2);
let (first_block, version) = futures::try_join!(read_first_block, get_next_blocks)?;
let version = version.ok_or(Error::NoSuchKey)?;
let order_stream = OrderTag::stream();
let first_block_hash = *first_block_hash;
let version_uuid = last_v.uuid;
let mut blocks = version
.blocks
.items()
.iter()
.map(|(_, vb)| (vb.hash, None))
.collect::<Vec<_>>();
blocks[0].1 = Some(first_block);
tokio::spawn(async move {
match async {
let garage2 = garage.clone();
let version_fut = tokio::spawn(async move {
garage2.version_table.get(&version_uuid, &EmptyKey).await
});
let body_stream = futures::stream::iter(blocks)
.map(move |(hash, data_opt)| {
let garage = garage.clone();
async move {
if let Some(data) = data_opt {
Ok(Bytes::from(data))
} else {
garage
.block_manager
.rpc_get_block(&hash)
.await
.map(Bytes::from)
}
let stream_block_0 = garage
.block_manager
.rpc_get_block_streaming(&first_block_hash, Some(order_stream.order(0)))
.await?;
tx.send(stream_block_0)
.await
.ok_or_message("channel closed")?;
let version = version_fut.await.unwrap()?.ok_or(Error::NoSuchKey)?;
for (i, (_, vb)) in version.blocks.items().iter().enumerate().skip(1) {
let stream_block_i = garage
.block_manager
.rpc_get_block_streaming(&vb.hash, Some(order_stream.order(i as u64)))
.await?;
tx.send(stream_block_i)
.await
.ok_or_message("channel closed")?;
}
})
.buffered(2);
Ok::<(), Error>(())
}
.await
{
Ok(()) => (),
Err(e) => {
let err = std::io::Error::new(
std::io::ErrorKind::Other,
format!("Error while getting object data: {}", e),
);
let _ = tx
.send(Box::pin(stream::once(future::ready(Err(err)))))
.await;
}
}
});
let body_stream = tokio_stream::wrappers::ReceiverStream::new(rx).flatten();
let body = hyper::body::Body::wrap_stream(body_stream);
Ok(resp_builder.body(body)?)
@@ -422,40 +445,79 @@ fn body_from_blocks_range(
all_blocks.len(),
4 + ((end - begin) / std::cmp::max(all_blocks[0].1.size as u64, 1024)) as usize,
));
let mut true_offset = 0;
let mut block_offset: u64 = 0;
for (_, b) in all_blocks.iter() {
if true_offset >= end {
if block_offset >= end {
break;
}
// Keep only blocks that have an intersection with the requested range
if true_offset < end && true_offset + b.size > begin {
blocks.push((*b, true_offset));
if block_offset < end && block_offset + b.size > begin {
blocks.push((*b, block_offset));
}
true_offset += b.size;
block_offset += b.size as u64;
}
let order_stream = OrderTag::stream();
let body_stream = futures::stream::iter(blocks)
.map(move |(block, true_offset)| {
.enumerate()
.map(move |(i, (block, block_offset))| {
let garage = garage.clone();
async move {
let data = garage.block_manager.rpc_get_block(&block.hash).await?;
let data = Bytes::from(data);
let start_in_block = if true_offset > begin {
0
} else {
begin - true_offset
};
let end_in_block = if true_offset + block.size < end {
block.size
} else {
end - true_offset
};
Result::<Bytes, Error>::Ok(
data.slice(start_in_block as usize..end_in_block as usize),
)
garage
.block_manager
.rpc_get_block_streaming(&block.hash, Some(order_stream.order(i as u64)))
.await
.unwrap_or_else(|e| error_stream(i, e))
.scan(block_offset, move |chunk_offset, chunk| {
let r = match chunk {
Ok(chunk_bytes) => {
let chunk_len = chunk_bytes.len() as u64;
let r = if *chunk_offset >= end {
// The current chunk is after the part we want to read.
// Returning None here will stop the scan, the rest of the
// stream will be ignored
None
} else if *chunk_offset + chunk_len <= begin {
// The current chunk is before the part we want to read.
// We return a None that will be removed by the filter_map
// below.
Some(None)
} else {
// The chunk has an intersection with the requested range
let start_in_chunk = if *chunk_offset > begin {
0
} else {
begin - *chunk_offset
};
let end_in_chunk = if *chunk_offset + chunk_len < end {
chunk_len
} else {
end - *chunk_offset
};
Some(Some(Ok(chunk_bytes
.slice(start_in_chunk as usize..end_in_chunk as usize))))
};
*chunk_offset += chunk_bytes.len() as u64;
r
}
Err(e) => Some(Some(Err(e))),
};
futures::future::ready(r)
})
.filter_map(futures::future::ready)
}
})
.buffered(2);
.buffered(2)
.flatten();
hyper::body::Body::wrap_stream(body_stream)
}
fn error_stream(i: usize, e: garage_util::error::Error) -> ByteStream {
Box::pin(futures::stream::once(async move {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Could not get block {}: {}", i, e),
))
}))
}
+42 -22
View File
@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc;
use futures::prelude::*;
@@ -8,7 +8,14 @@ use hyper::{Request, Response};
use md5::{digest::generic_array::*, Digest as Md5Digest, Md5};
use sha2::Sha256;
use opentelemetry::{
trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer},
Context,
};
use garage_rpc::netapp::bytes_buf::BytesBuf;
use garage_table::*;
use garage_util::async_hash::*;
use garage_util::data::*;
use garage_util::error::Error as GarageError;
use garage_util::time::*;
@@ -102,7 +109,7 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
size,
etag: data_md5sum_hex.clone(),
},
first_block,
first_block.to_vec(),
)),
};
@@ -130,7 +137,7 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
garage.version_table.insert(&version).await?;
// Transfer data and verify checksum
let first_block_hash = blake2sum(&first_block[..]);
let first_block_hash = async_blake2sum(first_block.clone()).await;
let tx_result = (|| async {
let (total_size, data_md5sum, data_sha256sum) = read_and_put_blocks(
@@ -273,14 +280,23 @@ async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
garage: &Garage,
version: &Version,
part_number: u64,
first_block: Vec<u8>,
first_block: Bytes,
first_block_hash: Hash,
chunker: &mut StreamChunker<S>,
) -> Result<(u64, GenericArray<u8, typenum::U16>, Hash), Error> {
let mut md5hasher = Md5::new();
let mut sha256hasher = Sha256::new();
md5hasher.update(&first_block[..]);
sha256hasher.update(&first_block[..]);
let tracer = opentelemetry::global::tracer("garage");
let md5hasher = AsyncHasher::<Md5>::new();
let sha256hasher = AsyncHasher::<Sha256>::new();
futures::future::join(
md5hasher.update(first_block.clone()),
sha256hasher.update(first_block.clone()),
)
.with_context(Context::current_with_span(
tracer.start("Hash first block (md5, sha256)"),
))
.await;
let mut next_offset = first_block.len();
let mut put_curr_version_block = put_block_meta(
@@ -302,9 +318,15 @@ async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
chunker.next(),
)?;
if let Some(block) = next_block {
md5hasher.update(&block[..]);
sha256hasher.update(&block[..]);
let block_hash = blake2sum(&block[..]);
let (_, _, block_hash) = futures::future::join3(
md5hasher.update(block.clone()),
sha256hasher.update(block.clone()),
async_blake2sum(block.clone()),
)
.with_context(Context::current_with_span(
tracer.start("Hash block (md5, sha256, blake2)"),
))
.await;
let block_len = block.len();
put_curr_version_block = put_block_meta(
garage,
@@ -322,9 +344,9 @@ async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
}
let total_size = next_offset as u64;
let data_md5sum = md5hasher.finalize();
let data_md5sum = md5hasher.finalize().await;
let data_sha256sum = sha256hasher.finalize();
let data_sha256sum = sha256hasher.finalize().await;
let data_sha256sum = Hash::try_from(&data_sha256sum[..]).unwrap();
Ok((total_size, data_md5sum, data_sha256sum))
@@ -364,7 +386,7 @@ struct StreamChunker<S: Stream<Item = Result<Bytes, Error>>> {
stream: S,
read_all: bool,
block_size: usize,
buf: VecDeque<u8>,
buf: BytesBuf,
}
impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
@@ -373,11 +395,11 @@ impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
stream,
read_all: false,
block_size,
buf: VecDeque::with_capacity(2 * block_size),
buf: BytesBuf::new(),
}
}
async fn next(&mut self) -> Result<Option<Vec<u8>>, Error> {
async fn next(&mut self) -> Result<Option<Bytes>, Error> {
while !self.read_all && self.buf.len() < self.block_size {
if let Some(block) = self.stream.next().await {
let bytes = block?;
@@ -390,12 +412,8 @@ impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
if self.buf.is_empty() {
Ok(None)
} else if self.buf.len() <= self.block_size {
let block = self.buf.drain(..).collect::<Vec<u8>>();
Ok(Some(block))
} else {
let block = self.buf.drain(..self.block_size).collect::<Vec<u8>>();
Ok(Some(block))
Ok(Some(self.buf.take_max(self.block_size)))
}
}
}
@@ -504,7 +522,9 @@ pub async fn handle_put_part(
// Copy block to store
let version = Version::new(version_uuid, bucket_id, key, false);
let first_block_hash = blake2sum(&first_block[..]);
let first_block_hash = async_blake2sum(first_block.clone()).await;
let (_, data_md5sum, data_sha256sum) = read_and_put_blocks(
&garage,
&version,
+21 -21
View File
@@ -25,7 +25,7 @@ impl From<&str> for Value {
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct IntValue(#[serde(rename = "$value")] pub i64);
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct Bucket {
#[serde(rename = "CreationDate")]
pub creation_date: Value,
@@ -33,7 +33,7 @@ pub struct Bucket {
pub name: Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct Owner {
#[serde(rename = "DisplayName")]
pub display_name: Value,
@@ -41,13 +41,13 @@ pub struct Owner {
pub id: Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct BucketList {
#[serde(rename = "Bucket")]
pub entries: Vec<Bucket>,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct ListAllMyBucketsResult {
#[serde(rename = "Buckets")]
pub buckets: BucketList,
@@ -55,7 +55,7 @@ pub struct ListAllMyBucketsResult {
pub owner: Owner,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct LocationConstraint {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
@@ -63,7 +63,7 @@ pub struct LocationConstraint {
pub region: String,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct Deleted {
#[serde(rename = "Key")]
pub key: Value,
@@ -73,7 +73,7 @@ pub struct Deleted {
pub delete_marker_version_id: Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct Error {
#[serde(rename = "Code")]
pub code: Value,
@@ -85,7 +85,7 @@ pub struct Error {
pub region: Option<Value>,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct DeleteError {
#[serde(rename = "Code")]
pub code: Value,
@@ -97,7 +97,7 @@ pub struct DeleteError {
pub version_id: Option<Value>,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct DeleteResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
@@ -107,7 +107,7 @@ pub struct DeleteResult {
pub errors: Vec<DeleteError>,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct InitiateMultipartUploadResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
@@ -119,7 +119,7 @@ pub struct InitiateMultipartUploadResult {
pub upload_id: Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct CompleteMultipartUploadResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
@@ -133,7 +133,7 @@ pub struct CompleteMultipartUploadResult {
pub etag: Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct Initiator {
#[serde(rename = "DisplayName")]
pub display_name: Value,
@@ -141,7 +141,7 @@ pub struct Initiator {
pub id: Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct ListMultipartItem {
#[serde(rename = "Initiated")]
pub initiated: Value,
@@ -157,7 +157,7 @@ pub struct ListMultipartItem {
pub storage_class: Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct ListMultipartUploadsResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
@@ -187,7 +187,7 @@ pub struct ListMultipartUploadsResult {
pub encoding_type: Option<Value>,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct PartItem {
#[serde(rename = "ETag")]
pub etag: Value,
@@ -199,7 +199,7 @@ pub struct PartItem {
pub size: IntValue,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct ListPartsResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
@@ -227,7 +227,7 @@ pub struct ListPartsResult {
pub storage_class: Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct ListBucketItem {
#[serde(rename = "Key")]
pub key: Value,
@@ -241,13 +241,13 @@ pub struct ListBucketItem {
pub storage_class: Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct CommonPrefix {
#[serde(rename = "Prefix")]
pub prefix: Value,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct ListBucketResult {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
@@ -281,7 +281,7 @@ pub struct ListBucketResult {
pub common_prefixes: Vec<CommonPrefix>,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct VersioningConfiguration {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
@@ -289,7 +289,7 @@ pub struct VersioningConfiguration {
pub status: Option<Value>,
}
#[derive(Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct PostObject {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
+7 -7
View File
@@ -1,5 +1,5 @@
use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac, NewMac};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use garage_util::data::{sha256sum, Hash};
@@ -29,17 +29,17 @@ pub fn signing_hmac(
secret_key: &str,
region: &str,
service: &str,
) -> Result<HmacSha256, crypto_mac::InvalidKeyLength> {
) -> Result<HmacSha256, crypto_common::InvalidLength> {
let secret = String::from("AWS4") + secret_key;
let mut date_hmac = HmacSha256::new_varkey(secret.as_bytes())?;
let mut date_hmac = HmacSha256::new_from_slice(secret.as_bytes())?;
date_hmac.update(datetime.format(SHORT_DATE).to_string().as_bytes());
let mut region_hmac = HmacSha256::new_varkey(&date_hmac.finalize().into_bytes())?;
let mut region_hmac = HmacSha256::new_from_slice(&date_hmac.finalize().into_bytes())?;
region_hmac.update(region.as_bytes());
let mut service_hmac = HmacSha256::new_varkey(&region_hmac.finalize().into_bytes())?;
let mut service_hmac = HmacSha256::new_from_slice(&region_hmac.finalize().into_bytes())?;
service_hmac.update(service.as_bytes());
let mut signing_hmac = HmacSha256::new_varkey(&service_hmac.finalize().into_bytes())?;
let mut signing_hmac = HmacSha256::new_from_slice(&service_hmac.finalize().into_bytes())?;
signing_hmac.update(b"aws4_request");
let hmac = HmacSha256::new_varkey(&signing_hmac.finalize().into_bytes())?;
let hmac = HmacSha256::new_from_slice(&signing_hmac.finalize().into_bytes())?;
Ok(hmac)
}
+10 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "garage_block"
version = "0.7.0"
version = "0.8.0"
authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018"
license = "AGPL-3.0"
@@ -15,9 +15,9 @@ path = "lib.rs"
[dependencies]
garage_db = { version = "0.8.0", path = "../db" }
garage_rpc = { version = "0.7.0", path = "../rpc" }
garage_util = { version = "0.7.0", path = "../util" }
garage_table = { version = "0.7.0", path = "../table" }
garage_rpc = { version = "0.8.0", path = "../rpc" }
garage_util = { version = "0.8.0", path = "../util" }
garage_table = { version = "0.8.0", path = "../table" }
opentelemetry = "0.17"
@@ -27,6 +27,8 @@ bytes = "1.0"
hex = "0.4"
tracing = "0.1.30"
rand = "0.8"
async-compression = { version = "0.3", features = ["tokio", "zstd"] }
zstd = { version = "0.9", default-features = false }
rmp-serde = "0.15"
@@ -36,3 +38,7 @@ serde_bytes = "0.11"
futures = "0.3"
futures-util = "0.3"
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
tokio-util = { version = "0.6", features = ["io"] }
[features]
system-libs = [ "zstd/pkg-config" ]
+36 -12
View File
@@ -1,16 +1,22 @@
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use zstd::stream::{decode_all as zstd_decode, Encoder};
use garage_util::data::*;
use garage_util::error::*;
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub enum DataBlockHeader {
Plain,
Compressed,
}
/// A possibly compressed block of data
#[derive(Debug, Serialize, Deserialize)]
pub enum DataBlock {
/// Uncompressed data
Plain(#[serde(with = "serde_bytes")] Vec<u8>),
Plain(Bytes),
/// Data compressed with zstd
Compressed(#[serde(with = "serde_bytes")] Vec<u8>),
Compressed(Bytes),
}
impl DataBlock {
@@ -30,7 +36,7 @@ impl DataBlock {
/// Get the buffer, possibly decompressing it, and verify it's integrity.
/// For Plain block, data is compared to hash, for Compressed block, zstd checksumming system
/// is used instead.
pub fn verify_get(self, hash: Hash) -> Result<Vec<u8>, Error> {
pub fn verify_get(self, hash: Hash) -> Result<Bytes, Error> {
match self {
DataBlock::Plain(data) => {
if blake2sum(&data) == hash {
@@ -39,9 +45,9 @@ impl DataBlock {
Err(Error::CorruptData(hash))
}
}
DataBlock::Compressed(data) => {
zstd_decode(&data[..]).map_err(|_| Error::CorruptData(hash))
}
DataBlock::Compressed(data) => zstd_decode(&data[..])
.map_err(|_| Error::CorruptData(hash))
.map(Bytes::from),
}
}
@@ -61,13 +67,31 @@ impl DataBlock {
}
}
pub fn from_buffer(data: Vec<u8>, level: Option<i32>) -> DataBlock {
if let Some(level) = level {
if let Ok(data) = zstd_encode(&data[..], level) {
return DataBlock::Compressed(data);
pub async fn from_buffer(data: Bytes, level: Option<i32>) -> DataBlock {
tokio::task::spawn_blocking(move || {
if let Some(level) = level {
if let Ok(data) = zstd_encode(&data[..], level) {
return DataBlock::Compressed(data.into());
}
}
DataBlock::Plain(data)
})
.await
.unwrap()
}
pub fn into_parts(self) -> (DataBlockHeader, Bytes) {
match self {
DataBlock::Plain(data) => (DataBlockHeader::Plain, data),
DataBlock::Compressed(data) => (DataBlockHeader::Compressed, data),
}
}
pub fn from_parts(h: DataBlockHeader, bytes: Bytes) -> Self {
match h {
DataBlockHeader::Plain => DataBlock::Plain(bytes),
DataBlockHeader::Compressed => DataBlock::Compressed(bytes),
}
DataBlock::Plain(data)
}
}
+1
View File
@@ -3,6 +3,7 @@ extern crate tracing;
pub mod manager;
pub mod repair;
pub mod resync;
mod block;
mod metrics;
+277 -560
View File
File diff suppressed because it is too large Load Diff
+28 -6
View File
@@ -19,7 +19,17 @@ use garage_util::tranquilizer::Tranquilizer;
use crate::manager::*;
const SCRUB_INTERVAL: Duration = Duration::from_secs(3600 * 24 * 30); // full scrub every 30 days
// Full scrub every 30 days
const SCRUB_INTERVAL: Duration = Duration::from_secs(3600 * 24 * 30);
// Scrub tranquility is initially set to 4, but can be changed in the CLI
// and the updated version is persisted over Garage restarts
const INITIAL_SCRUB_TRANQUILITY: u32 = 4;
// ---- ---- ----
// FIRST KIND OF REPAIR: FINDING MISSING BLOCKS/USELESS BLOCKS
// This is a one-shot repair operation that can be launched,
// checks everything, and then exits.
// ---- ---- ----
pub struct RepairWorker {
manager: Arc<BlockManager>,
@@ -102,7 +112,9 @@ impl Worker for RepairWorker {
}
for hash in batch_of_hashes.into_iter() {
self.manager.put_to_resync(&hash, Duration::from_secs(0))?;
self.manager
.resync
.put_to_resync(&hash, Duration::from_secs(0))?;
self.next_start = Some(hash)
}
@@ -114,7 +126,9 @@ impl Worker for RepairWorker {
// This allows us to find blocks we are storing but don't actually need,
// so that we can offload them if necessary and then delete them locally.
if let Some(hash) = bi.next().await? {
self.manager.put_to_resync(&hash, Duration::from_secs(0))?;
self.manager
.resync
.put_to_resync(&hash, Duration::from_secs(0))?;
Ok(WorkerState::Busy)
} else {
Ok(WorkerState::Done)
@@ -128,7 +142,13 @@ impl Worker for RepairWorker {
}
}
// ----
// ---- ---- ----
// SECOND KIND OF REPAIR: SCRUBBING THE DATASTORE
// This is significantly more complex than the process above,
// as it is a continuously-running task that triggers automatically
// every SCRUB_INTERVAL, but can also be triggered manually
// and whose parameter (esp. speed) can be controlled at runtime.
// ---- ---- ----
pub struct ScrubWorker {
manager: Arc<BlockManager>,
@@ -176,7 +196,7 @@ impl ScrubWorker {
Ok(v) => v,
Err(_) => ScrubWorkerPersisted {
time_last_complete_scrub: 0,
tranquility: 4,
tranquility: INITIAL_SCRUB_TRANQUILITY,
corruptions_detected: 0,
},
};
@@ -343,7 +363,9 @@ impl Worker for ScrubWorker {
}
}
// ----
// ---- ---- ----
// UTILITY FOR ENUMERATING THE BLOCK STORE
// ---- ---- ----
struct BlockStoreIterator {
path: Vec<ReadingDir>,
+589
View File
@@ -0,0 +1,589 @@
use std::collections::HashSet;
use std::convert::TryInto;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use arc_swap::ArcSwap;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::select;
use tokio::sync::{watch, Notify};
use opentelemetry::{
trace::{FutureExt as OtelFutureExt, TraceContextExt, Tracer},
Context, KeyValue,
};
use garage_db as db;
use garage_db::counted_tree_hack::CountedTree;
use garage_util::background::*;
use garage_util::data::*;
use garage_util::error::*;
use garage_util::metrics::RecordDuration;
use garage_util::persister::Persister;
use garage_util::time::*;
use garage_util::tranquilizer::Tranquilizer;
use garage_rpc::system::System;
use garage_rpc::*;
use garage_table::replication::TableReplication;
use crate::manager::*;
// The delay between the time where a resync operation fails
// and the time when it is retried, with exponential backoff
// (multiplied by 2, 4, 8, 16, etc. for every consecutive failure).
pub(crate) const RESYNC_RETRY_DELAY: Duration = Duration::from_secs(60);
// The minimum retry delay is 60 seconds = 1 minute
// The maximum retry delay is 60 seconds * 2^6 = 60 seconds << 6 = 64 minutes (~1 hour)
pub(crate) const RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER: u64 = 6;
// No more than 4 resync workers can be running in the system
pub(crate) const MAX_RESYNC_WORKERS: usize = 4;
// Resync tranquility is initially set to 2, but can be changed in the CLI
// and the updated version is persisted over Garage restarts
const INITIAL_RESYNC_TRANQUILITY: u32 = 2;
pub struct BlockResyncManager {
pub(crate) queue: CountedTree,
pub(crate) notify: Notify,
pub(crate) errors: CountedTree,
busy_set: BusySet,
persister: Persister<ResyncPersistedConfig>,
persisted: ArcSwap<ResyncPersistedConfig>,
}
#[derive(Serialize, Deserialize, Clone, Copy)]
struct ResyncPersistedConfig {
n_workers: usize,
tranquility: u32,
}
enum ResyncIterResult {
BusyDidSomething,
BusyDidNothing,
IdleFor(Duration),
}
type BusySet = Arc<Mutex<HashSet<Vec<u8>>>>;
struct BusyBlock {
time_bytes: Vec<u8>,
hash_bytes: Vec<u8>,
busy_set: BusySet,
}
impl BlockResyncManager {
pub(crate) fn new(db: &db::Db, system: &System) -> Self {
let queue = db
.open_tree("block_local_resync_queue")
.expect("Unable to open block_local_resync_queue tree");
let queue = CountedTree::new(queue).expect("Could not count block_local_resync_queue");
let errors = db
.open_tree("block_local_resync_errors")
.expect("Unable to open block_local_resync_errors tree");
let errors = CountedTree::new(errors).expect("Could not count block_local_resync_errors");
let persister = Persister::new(&system.metadata_dir, "resync_cfg");
let persisted = match persister.load() {
Ok(v) => v,
Err(_) => ResyncPersistedConfig {
n_workers: 1,
tranquility: INITIAL_RESYNC_TRANQUILITY,
},
};
Self {
queue,
notify: Notify::new(),
errors,
busy_set: Arc::new(Mutex::new(HashSet::new())),
persister,
persisted: ArcSwap::new(Arc::new(persisted)),
}
}
/// Get lenght of resync queue
pub fn queue_len(&self) -> Result<usize, Error> {
// This currently can't return an error because the CountedTree hack
// doesn't error on .len(), but this will change when we remove the hack
// (hopefully someday!)
Ok(self.queue.len())
}
/// Get number of blocks that have an error
pub fn errors_len(&self) -> Result<usize, Error> {
// (see queue_len comment)
Ok(self.errors.len())
}
// ---- Resync loop ----
// This part manages a queue of blocks that need to be
// "resynchronized", i.e. that need to have a check that
// they are at present if we need them, or that they are
// deleted once the garbage collection delay has passed.
//
// Here are some explanations on how the resync queue works.
// There are two Sled trees that are used to have information
// about the status of blocks that need to be resynchronized:
//
// - resync.queue: a tree that is ordered first by a timestamp
// (in milliseconds since Unix epoch) that is the time at which
// the resync must be done, and second by block hash.
// The key in this tree is just:
// concat(timestamp (8 bytes), hash (32 bytes))
// The value is the same 32-byte hash.
//
// - resync.errors: a tree that indicates for each block
// if the last resync resulted in an error, and if so,
// the following two informations (see the ErrorCounter struct):
// - how many consecutive resync errors for this block?
// - when was the last try?
// These two informations are used to implement an
// exponential backoff retry strategy.
// The key in this tree is the 32-byte hash of the block,
// and the value is the encoded ErrorCounter value.
//
// We need to have these two trees, because the resync queue
// is not just a queue of items to process, but a set of items
// that are waiting a specific delay until we can process them
// (the delay being necessary both internally for the exponential
// backoff strategy, and exposed as a parameter when adding items
// to the queue, e.g. to wait until the GC delay has passed).
// This is why we need one tree ordered by time, and one
// ordered by identifier of item to be processed (block hash).
//
// When the worker wants to process an item it takes from
// resync.queue, it checks in resync.errors that if there is an
// exponential back-off delay to await, it has passed before we
// process the item. If not, the item in the queue is skipped
// (but added back for later processing after the time of the
// delay).
//
// An alternative that would have seemed natural is to
// only add items to resync.queue with a processing time that is
// after the delay, but there are several issues with this:
// - This requires to synchronize updates to resync.queue and
// resync.errors (with the current model, there is only one thread,
// the worker thread, that accesses resync.errors,
// so no need to synchronize) by putting them both in a lock.
// This would mean that block_incref might need to take a lock
// before doing its thing, meaning it has much more chances of
// not completing successfully if something bad happens to Garage.
// Currently Garage is not able to recover from block_incref that
// doesn't complete successfully, because it is necessary to ensure
// the consistency between the state of the block manager and
// information in the BlockRef table.
// - If a resync fails, we put that block in the resync.errors table,
// and also add it back to resync.queue to be processed after
// the exponential back-off delay,
// but maybe the block is already scheduled to be resynced again
// at another time that is before the exponential back-off delay,
// and we have no way to check that easily. This means that
// in all cases, we need to check the resync.errors table
// in the resync loop at the time when a block is popped from
// the resync.queue.
// Overall, the current design is therefore simpler and more robust
// because it tolerates inconsistencies between the resync.queue
// and resync.errors table (items being scheduled in resync.queue
// for times that are earlier than the exponential back-off delay
// is a natural condition that is handled properly).
pub(crate) fn put_to_resync(&self, hash: &Hash, delay: Duration) -> db::Result<()> {
let when = now_msec() + delay.as_millis() as u64;
self.put_to_resync_at(hash, when)
}
pub(crate) fn put_to_resync_at(&self, hash: &Hash, when: u64) -> db::Result<()> {
trace!("Put resync_queue: {} {:?}", when, hash);
let mut key = u64::to_be_bytes(when).to_vec();
key.extend(hash.as_ref());
self.queue.insert(key, hash.as_ref())?;
self.notify.notify_waiters();
Ok(())
}
async fn resync_iter(&self, manager: &BlockManager) -> Result<ResyncIterResult, db::Error> {
if let Some(block) = self.get_block_to_resync()? {
let time_msec = u64::from_be_bytes(block.time_bytes[0..8].try_into().unwrap());
let now = now_msec();
if now >= time_msec {
let hash = Hash::try_from(&block.hash_bytes[..]).unwrap();
if let Some(ec) = self.errors.get(hash.as_slice())? {
let ec = ErrorCounter::decode(&ec);
if now < ec.next_try() {
// if next retry after an error is not yet,
// don't do resync and return early, but still
// make sure the item is still in queue at expected time
self.put_to_resync_at(&hash, ec.next_try())?;
// ec.next_try() > now >= time_msec, so this remove
// is not removing the one we added just above
// (we want to do the remove after the insert to ensure
// that the item is not lost if we crash in-between)
self.queue.remove(&block.time_bytes)?;
return Ok(ResyncIterResult::BusyDidNothing);
}
}
let tracer = opentelemetry::global::tracer("garage");
let trace_id = gen_uuid();
let span = tracer
.span_builder("Resync block")
.with_trace_id(
opentelemetry::trace::TraceId::from_hex(&hex::encode(
&trace_id.as_slice()[..16],
))
.unwrap(),
)
.with_attributes(vec![KeyValue::new("block", format!("{:?}", hash))])
.start(&tracer);
let res = self
.resync_block(manager, &hash)
.with_context(Context::current_with_span(span))
.bound_record_duration(&manager.metrics.resync_duration)
.await;
manager.metrics.resync_counter.add(1);
if let Err(e) = &res {
manager.metrics.resync_error_counter.add(1);
warn!("Error when resyncing {:?}: {}", hash, e);
let err_counter = match self.errors.get(hash.as_slice())? {
Some(ec) => ErrorCounter::decode(&ec).add1(now + 1),
None => ErrorCounter::new(now + 1),
};
self.errors.insert(hash.as_slice(), err_counter.encode())?;
self.put_to_resync_at(&hash, err_counter.next_try())?;
// err_counter.next_try() >= now + 1 > now,
// the entry we remove from the queue is not
// the entry we inserted with put_to_resync_at
self.queue.remove(&block.time_bytes)?;
} else {
self.errors.remove(hash.as_slice())?;
self.queue.remove(&block.time_bytes)?;
}
Ok(ResyncIterResult::BusyDidSomething)
} else {
Ok(ResyncIterResult::IdleFor(Duration::from_millis(
time_msec - now,
)))
}
} else {
// Here we wait either for a notification that an item has been
// added to the queue, or for a constant delay of 10 secs to expire.
// The delay avoids a race condition where the notification happens
// between the time we checked the queue and the first poll
// to resync_notify.notified(): if that happens, we'll just loop
// back 10 seconds later, which is fine.
Ok(ResyncIterResult::IdleFor(Duration::from_secs(10)))
}
}
fn get_block_to_resync(&self) -> Result<Option<BusyBlock>, db::Error> {
let mut busy = self.busy_set.lock().unwrap();
for it in self.queue.iter()? {
let (time_bytes, hash_bytes) = it?;
if !busy.contains(&time_bytes) {
busy.insert(time_bytes.clone());
return Ok(Some(BusyBlock {
time_bytes,
hash_bytes,
busy_set: self.busy_set.clone(),
}));
}
}
Ok(None)
}
async fn resync_block(&self, manager: &BlockManager, hash: &Hash) -> Result<(), Error> {
let BlockStatus { exists, needed } = manager.check_block_status(hash).await?;
if exists != needed.is_needed() || exists != needed.is_nonzero() {
debug!(
"Resync block {:?}: exists {}, nonzero rc {}, deletable {}",
hash,
exists,
needed.is_nonzero(),
needed.is_deletable(),
);
}
if exists && needed.is_deletable() {
info!("Resync block {:?}: offloading and deleting", hash);
let mut who = manager.replication.write_nodes(hash);
if who.len() < manager.replication.write_quorum() {
return Err(Error::Message("Not trying to offload block because we don't have a quorum of nodes to write to".to_string()));
}
who.retain(|id| *id != manager.system.id);
let who_needs_resps = manager
.system
.rpc
.call_many(
&manager.endpoint,
&who,
BlockRpc::NeedBlockQuery(*hash),
RequestStrategy::with_priority(PRIO_BACKGROUND),
)
.await?;
let mut need_nodes = vec![];
for (node, needed) in who_needs_resps {
match needed.err_context("NeedBlockQuery RPC")? {
BlockRpc::NeedBlockReply(needed) => {
if needed {
need_nodes.push(node);
}
}
m => {
return Err(Error::unexpected_rpc_message(m));
}
}
}
if !need_nodes.is_empty() {
trace!(
"Block {:?} needed by {} nodes, sending",
hash,
need_nodes.len()
);
for node in need_nodes.iter() {
manager
.metrics
.resync_send_counter
.add(1, &[KeyValue::new("to", format!("{:?}", node))]);
}
let block = manager.read_block(hash).await?;
let (header, bytes) = block.into_parts();
let put_block_message = Req::new(BlockRpc::PutBlock {
hash: *hash,
header,
})?
.with_stream_from_buffer(bytes);
manager
.system
.rpc
.try_call_many(
&manager.endpoint,
&need_nodes[..],
put_block_message,
RequestStrategy::with_priority(PRIO_BACKGROUND)
.with_quorum(need_nodes.len()),
)
.await
.err_context("PutBlock RPC")?;
}
info!(
"Deleting unneeded block {:?}, offload finished ({} / {})",
hash,
need_nodes.len(),
who.len()
);
manager.delete_if_unneeded(hash).await?;
manager.rc.clear_deleted_block_rc(hash)?;
}
if needed.is_nonzero() && !exists {
info!(
"Resync block {:?}: fetching absent but needed block (refcount > 0)",
hash
);
let block_data = manager.rpc_get_raw_block(hash, None).await?;
manager.metrics.resync_recv_counter.add(1);
manager.write_block(hash, &block_data).await?;
}
Ok(())
}
async fn update_persisted(
&self,
update: impl Fn(&mut ResyncPersistedConfig),
) -> Result<(), Error> {
let mut cfg: ResyncPersistedConfig = *self.persisted.load().as_ref();
update(&mut cfg);
self.persister.save_async(&cfg).await?;
self.persisted.store(Arc::new(cfg));
self.notify.notify_waiters();
Ok(())
}
pub async fn set_n_workers(&self, n_workers: usize) -> Result<(), Error> {
if !(1..=MAX_RESYNC_WORKERS).contains(&n_workers) {
return Err(Error::Message(format!(
"Invalid number of resync workers, must be between 1 and {}",
MAX_RESYNC_WORKERS
)));
}
self.update_persisted(|cfg| cfg.n_workers = n_workers).await
}
pub async fn set_tranquility(&self, tranquility: u32) -> Result<(), Error> {
self.update_persisted(|cfg| cfg.tranquility = tranquility)
.await
}
}
impl Drop for BusyBlock {
fn drop(&mut self) {
let mut busy = self.busy_set.lock().unwrap();
busy.remove(&self.time_bytes);
}
}
pub(crate) struct ResyncWorker {
index: usize,
manager: Arc<BlockManager>,
tranquilizer: Tranquilizer,
next_delay: Duration,
}
impl ResyncWorker {
pub(crate) fn new(index: usize, manager: Arc<BlockManager>) -> Self {
Self {
index,
manager,
tranquilizer: Tranquilizer::new(30),
next_delay: Duration::from_secs(10),
}
}
}
#[async_trait]
impl Worker for ResyncWorker {
fn name(&self) -> String {
format!("Block resync worker #{}", self.index + 1)
}
fn info(&self) -> Option<String> {
let persisted = self.manager.resync.persisted.load();
if self.index >= persisted.n_workers {
return Some("(unused)".into());
}
let mut ret = vec![];
ret.push(format!("tranquility = {}", persisted.tranquility));
let qlen = self.manager.resync.queue_len().unwrap_or(0);
if qlen > 0 {
ret.push(format!("{} blocks in queue", qlen));
}
let elen = self.manager.resync.errors_len().unwrap_or(0);
if elen > 0 {
ret.push(format!("{} blocks in error state", elen));
}
Some(ret.join(", "))
}
async fn work(&mut self, _must_exit: &mut watch::Receiver<bool>) -> Result<WorkerState, Error> {
if self.index >= self.manager.resync.persisted.load().n_workers {
return Ok(WorkerState::Idle);
}
self.tranquilizer.reset();
match self.manager.resync.resync_iter(&self.manager).await {
Ok(ResyncIterResult::BusyDidSomething) => Ok(self
.tranquilizer
.tranquilize_worker(self.manager.resync.persisted.load().tranquility)),
Ok(ResyncIterResult::BusyDidNothing) => Ok(WorkerState::Busy),
Ok(ResyncIterResult::IdleFor(delay)) => {
self.next_delay = delay;
Ok(WorkerState::Idle)
}
Err(e) => {
// The errors that we have here are only Sled errors
// We don't really know how to handle them so just ¯\_(ツ)_/¯
// (there is kind of an assumption that Sled won't error on us,
// if it does there is not much we can do -- TODO should we just panic?)
// Here we just give the error to the worker manager,
// it will print it to the logs and increment a counter
Err(e.into())
}
}
}
async fn wait_for_work(&mut self, _must_exit: &watch::Receiver<bool>) -> WorkerState {
while self.index >= self.manager.resync.persisted.load().n_workers {
self.manager.resync.notify.notified().await
}
select! {
_ = tokio::time::sleep(self.next_delay) => (),
_ = self.manager.resync.notify.notified() => (),
};
WorkerState::Busy
}
}
/// Counts the number of errors when resyncing a block,
/// and the time of the last try.
/// Used to implement exponential backoff.
#[derive(Clone, Copy, Debug)]
struct ErrorCounter {
errors: u64,
last_try: u64,
}
impl ErrorCounter {
fn new(now: u64) -> Self {
Self {
errors: 1,
last_try: now,
}
}
fn decode(data: &[u8]) -> Self {
Self {
errors: u64::from_be_bytes(data[0..8].try_into().unwrap()),
last_try: u64::from_be_bytes(data[8..16].try_into().unwrap()),
}
}
fn encode(&self) -> Vec<u8> {
[
u64::to_be_bytes(self.errors),
u64::to_be_bytes(self.last_try),
]
.concat()
}
fn add1(self, now: u64) -> Self {
Self {
errors: self.errors + 1,
last_try: now,
}
}
fn delay_msec(&self) -> u64 {
(RESYNC_RETRY_DELAY.as_millis() as u64)
<< std::cmp::min(self.errors - 1, RESYNC_RETRY_DELAY_MAX_BACKOFF_POWER)
}
fn next_try(&self) -> u64 {
self.last_try + self.delay_msec()
}
}
+6 -3
View File
@@ -21,9 +21,9 @@ err-derive = "0.3"
hexdump = "0.1"
tracing = "0.1.30"
heed = "0.11"
rusqlite = { version = "0.27", features = ["bundled"] }
sled = "0.34"
heed = { version = "0.11", default-features = false, features = ["lmdb"], optional = true }
rusqlite = { version = "0.27", optional = true }
sled = { version = "0.34", optional = true }
# cli deps
clap = { version = "3.1.18", optional = true, features = ["derive", "env"] }
@@ -33,4 +33,7 @@ pretty_env_logger = { version = "0.4", optional = true }
mktemp = "0.4"
[features]
bundled-libs = [ "rusqlite/bundled" ]
cli = ["clap", "pretty_env_logger"]
lmdb = [ "heed" ]
sqlite = [ "rusqlite" ]
+7
View File
@@ -1,8 +1,15 @@
#[macro_use]
#[cfg(feature = "sqlite")]
extern crate tracing;
#[cfg(not(any(feature = "lmdb", feature = "sled", feature = "sqlite")))]
compile_error!("Must activate the Cargo feature for at least one DB engine: lmdb, sled or sqlite.");
#[cfg(feature = "lmdb")]
pub mod lmdb_adapter;
#[cfg(feature = "sled")]
pub mod sled_adapter;
#[cfg(feature = "sqlite")]
pub mod sqlite_adapter;
pub mod counted_tree_hack;
+9 -4
View File
@@ -1,9 +1,5 @@
use crate::*;
use crate::lmdb_adapter::LmdbDb;
use crate::sled_adapter::SledDb;
use crate::sqlite_adapter::SqliteDb;
fn test_suite(db: Db) {
let tree = db.open_tree("tree").unwrap();
@@ -80,7 +76,10 @@ fn test_suite(db: Db) {
}
#[test]
#[cfg(feature = "lmdb")]
fn test_lmdb_db() {
use crate::lmdb_adapter::LmdbDb;
let path = mktemp::Temp::new_dir().unwrap();
let db = heed::EnvOpenOptions::new()
.max_dbs(100)
@@ -92,7 +91,10 @@ fn test_lmdb_db() {
}
#[test]
#[cfg(feature = "sled")]
fn test_sled_db() {
use crate::sled_adapter::SledDb;
let path = mktemp::Temp::new_dir().unwrap();
let db = SledDb::init(sled::open(path.to_path_buf()).unwrap());
test_suite(db);
@@ -100,7 +102,10 @@ fn test_sled_db() {
}
#[test]
#[cfg(feature = "sqlite")]
fn test_sqlite_db() {
use crate::sqlite_adapter::SqliteDb;
let db = SqliteDb::init(rusqlite::Connection::open_in_memory().unwrap());
test_suite(db);
}
+40 -19
View File
@@ -1,6 +1,6 @@
[package]
name = "garage"
version = "0.7.0"
version = "0.8.0"
authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018"
license = "AGPL-3.0"
@@ -22,20 +22,20 @@ path = "tests/lib.rs"
[dependencies]
garage_db = { version = "0.8.0", path = "../db" }
garage_api = { version = "0.7.0", path = "../api" }
garage_block = { version = "0.7.0", path = "../block" }
garage_model = { version = "0.7.0", path = "../model" }
garage_rpc = { version = "0.7.0", path = "../rpc" }
garage_table = { version = "0.7.0", path = "../table" }
garage_util = { version = "0.7.0", path = "../util" }
garage_web = { version = "0.7.0", path = "../web" }
garage_api = { version = "0.8.0", path = "../api" }
garage_block = { version = "0.8.0", path = "../block" }
garage_model = { version = "0.8.0", path = "../model" }
garage_rpc = { version = "0.8.0", path = "../rpc" }
garage_table = { version = "0.8.0", path = "../table" }
garage_util = { version = "0.8.0", path = "../util" }
garage_web = { version = "0.8.0", path = "../web" }
bytes = "1.0"
bytesize = "1.1"
timeago = "0.3"
hex = "0.4"
tracing = { version = "0.1.30", features = ["log-always"] }
pretty_env_logger = "0.4"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
rand = "0.8"
async-trait = "0.1.7"
sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" }
@@ -50,22 +50,20 @@ futures = "0.3"
futures-util = "0.3"
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
#netapp = { version = "0.3.2", git = "https://git.deuxfleurs.fr/lx/netapp" }
#netapp = { version = "0.4", path = "../../../netapp" }
netapp = "0.4"
netapp = "0.5"
opentelemetry = { version = "0.17", features = [ "rt-tokio" ] }
opentelemetry-prometheus = "0.10"
opentelemetry-otlp = "0.10"
prometheus = "0.13"
opentelemetry-prometheus = { version = "0.10", optional = true }
opentelemetry-otlp = { version = "0.10", optional = true }
prometheus = { version = "0.13", optional = true }
[dev-dependencies]
aws-sdk-s3 = "0.8"
aws-sdk-s3 = "0.19"
chrono = "0.4"
http = "0.2"
hmac = "0.10"
hmac = "0.12"
hyper = { version = "0.14", features = ["client", "http1", "runtime"] }
sha2 = "0.9"
sha2 = "0.10"
static_init = "1.0"
assert-json-diff = "2.0"
@@ -74,5 +72,28 @@ base64 = "0.13"
[features]
kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ]
default = [ "bundled-libs", "metrics", "sled" ]
k2v = [ "garage_util/k2v", "garage_api/k2v" ]
# Database engines, Sled is still our default even though we don't like it
sled = [ "garage_model/sled" ]
lmdb = [ "garage_model/lmdb" ]
sqlite = [ "garage_model/sqlite" ]
# Automatic registration and discovery via Consul API
consul-discovery = [ "garage_rpc/consul-discovery" ]
# Automatic registration and discovery via Kubernetes API
kubernetes-discovery = [ "garage_rpc/kubernetes-discovery" ]
# Prometheus exporter (/metrics endpoint).
metrics = [ "garage_api/metrics", "opentelemetry-prometheus", "prometheus" ]
# Exporter for the OpenTelemetry Collector.
telemetry-otlp = [ "opentelemetry-otlp" ]
# NOTE: bundled-libs and system-libs should be treat as mutually exclusive;
# exactly one of them should be enabled.
# Use bundled libsqlite instead of linking against system-provided.
bundled-libs = [ "garage_db/bundled-libs" ]
# Link against system-provided libsodium and libzstd.
system-libs = [ "garage_block/system-libs", "garage_rpc/system-libs", "sodiumoxide/use-pkg-config" ]
+37 -6
View File
@@ -15,6 +15,8 @@ use garage_table::*;
use garage_rpc::*;
use garage_block::repair::ScrubWorkerCommand;
use garage_model::bucket_alias_table::*;
use garage_model::bucket_table::*;
use garage_model::garage::Garage;
@@ -681,7 +683,7 @@ impl AdminRpcHandler {
.endpoint
.call(
&node,
&AdminRpc::LaunchRepair(opt_to_send.clone()),
AdminRpc::LaunchRepair(opt_to_send.clone()),
PRIO_NORMAL,
)
.await;
@@ -721,7 +723,7 @@ impl AdminRpcHandler {
let node_id = (*node).into();
match self
.endpoint
.call(&node_id, &AdminRpc::Stats(opt), PRIO_NORMAL)
.call(&node_id, AdminRpc::Stats(opt), PRIO_NORMAL)
.await?
{
Ok(AdminRpc::Ok(s)) => writeln!(&mut ret, "{}", s).unwrap(),
@@ -739,8 +741,11 @@ impl AdminRpcHandler {
let mut ret = String::new();
writeln!(
&mut ret,
"\nGarage version: {}",
self.garage.system.garage_version(),
"\nGarage version: {} [features: {}]",
garage_util::version::garage_version(),
garage_util::version::garage_features()
.map(|list| list.join(", "))
.unwrap_or_else(|| "(unknown)".into()),
)
.unwrap();
writeln!(&mut ret, "\nDatabase engine: {}", self.garage.db.engine()).unwrap();
@@ -779,13 +784,13 @@ impl AdminRpcHandler {
writeln!(
&mut ret,
" resync queue length: {}",
self.garage.block_manager.resync_queue_len()?
self.garage.block_manager.resync.queue_len()?
)
.unwrap();
writeln!(
&mut ret,
" blocks with resync errors: {}",
self.garage.block_manager.resync_errors_len()?
self.garage.block_manager.resync.errors_len()?
)
.unwrap();
@@ -836,6 +841,32 @@ impl AdminRpcHandler {
let workers = self.garage.background.get_worker_info();
Ok(AdminRpc::WorkerList(workers, opt))
}
WorkerCmd::Set { opt } => match opt {
WorkerSetCmd::ScrubTranquility { tranquility } => {
let scrub_command = ScrubWorkerCommand::SetTranquility(tranquility);
self.garage
.block_manager
.send_scrub_command(scrub_command)
.await;
Ok(AdminRpc::Ok("Scrub tranquility updated".into()))
}
WorkerSetCmd::ResyncNWorkers { n_workers } => {
self.garage
.block_manager
.resync
.set_n_workers(n_workers)
.await?;
Ok(AdminRpc::Ok("Number of resync workers updated".into()))
}
WorkerSetCmd::ResyncTranquility { tranquility } => {
self.garage
.block_manager
.resync
.set_tranquility(tranquility)
.await?;
Ok(AdminRpc::Ok("Resync tranquility updated".into()))
}
},
}
}
}
+3 -3
View File
@@ -47,7 +47,7 @@ pub async fn cli_command_dispatch(
pub async fn cmd_status(rpc_cli: &Endpoint<SystemRpc, ()>, rpc_host: NodeID) -> Result<(), Error> {
let status = match rpc_cli
.call(&rpc_host, &SystemRpc::GetKnownNodes, PRIO_NORMAL)
.call(&rpc_host, SystemRpc::GetKnownNodes, PRIO_NORMAL)
.await??
{
SystemRpc::ReturnKnownNodes(nodes) => nodes,
@@ -149,7 +149,7 @@ pub async fn cmd_connect(
args: ConnectNodeOpt,
) -> Result<(), Error> {
match rpc_cli
.call(&rpc_host, &SystemRpc::Connect(args.node), PRIO_NORMAL)
.call(&rpc_host, SystemRpc::Connect(args.node), PRIO_NORMAL)
.await??
{
SystemRpc::Ok => {
@@ -165,7 +165,7 @@ pub async fn cmd_admin(
rpc_host: NodeID,
args: AdminRpc,
) -> Result<(), HelperError> {
match rpc_cli.call(&rpc_host, &args, PRIO_NORMAL).await?? {
match rpc_cli.call(&rpc_host, args, PRIO_NORMAL).await?? {
AdminRpc::Ok(msg) => {
println!("{}", msg);
}
+3 -3
View File
@@ -36,7 +36,7 @@ pub async fn cmd_assign_role(
args: AssignRoleOpt,
) -> Result<(), Error> {
let status = match rpc_cli
.call(&rpc_host, &SystemRpc::GetKnownNodes, PRIO_NORMAL)
.call(&rpc_host, SystemRpc::GetKnownNodes, PRIO_NORMAL)
.await??
{
SystemRpc::ReturnKnownNodes(nodes) => nodes,
@@ -245,7 +245,7 @@ pub async fn fetch_layout(
rpc_host: NodeID,
) -> Result<ClusterLayout, Error> {
match rpc_cli
.call(&rpc_host, &SystemRpc::PullClusterLayout, PRIO_NORMAL)
.call(&rpc_host, SystemRpc::PullClusterLayout, PRIO_NORMAL)
.await??
{
SystemRpc::AdvertiseClusterLayout(t) => Ok(t),
@@ -261,7 +261,7 @@ pub async fn send_layout(
rpc_cli
.call(
&rpc_host,
&SystemRpc::AdvertiseClusterLayout(layout),
SystemRpc::AdvertiseClusterLayout(layout),
PRIO_NORMAL,
)
.await??;
+71 -51
View File
@@ -1,64 +1,65 @@
use serde::{Deserialize, Serialize};
use structopt::StructOpt;
use garage_util::version::garage_version;
#[derive(StructOpt, Debug)]
pub enum Command {
/// Run Garage server
#[structopt(name = "server")]
#[structopt(name = "server", version = garage_version())]
Server,
/// Get network status
#[structopt(name = "status")]
#[structopt(name = "status", version = garage_version())]
Status,
/// Operations on individual Garage nodes
#[structopt(name = "node")]
#[structopt(name = "node", version = garage_version())]
Node(NodeOperation),
/// Operations on the assignation of node roles in the cluster layout
#[structopt(name = "layout")]
#[structopt(name = "layout", version = garage_version())]
Layout(LayoutOperation),
/// Operations on buckets
#[structopt(name = "bucket")]
#[structopt(name = "bucket", version = garage_version())]
Bucket(BucketOperation),
/// Operations on S3 access keys
#[structopt(name = "key")]
#[structopt(name = "key", version = garage_version())]
Key(KeyOperation),
/// Run migrations from previous Garage version
/// (DO NOT USE WITHOUT READING FULL DOCUMENTATION)
#[structopt(name = "migrate")]
#[structopt(name = "migrate", version = garage_version())]
Migrate(MigrateOpt),
/// Start repair of node data on remote node
#[structopt(name = "repair")]
#[structopt(name = "repair", version = garage_version())]
Repair(RepairOpt),
/// Offline reparation of node data (these repairs must be run offline
/// directly on the server node)
#[structopt(name = "offline-repair")]
#[structopt(name = "offline-repair", version = garage_version())]
OfflineRepair(OfflineRepairOpt),
/// Gather node statistics
#[structopt(name = "stats")]
#[structopt(name = "stats", version = garage_version())]
Stats(StatsOpt),
/// Manage background workers
#[structopt(name = "worker")]
#[structopt(name = "worker", version = garage_version())]
Worker(WorkerOpt),
}
#[derive(StructOpt, Debug)]
pub enum NodeOperation {
/// Print identifier (public key) of this Garage node
#[structopt(name = "id")]
#[structopt(name = "id", version = garage_version())]
NodeId(NodeIdOpt),
/// Connect to Garage node that is currently isolated from the system
#[structopt(name = "connect")]
#[structopt(name = "connect", version = garage_version())]
Connect(ConnectNodeOpt),
}
@@ -79,23 +80,23 @@ pub struct ConnectNodeOpt {
#[derive(StructOpt, Debug)]
pub enum LayoutOperation {
/// Assign role to Garage node
#[structopt(name = "assign")]
#[structopt(name = "assign", version = garage_version())]
Assign(AssignRoleOpt),
/// Remove role from Garage cluster node
#[structopt(name = "remove")]
#[structopt(name = "remove", version = garage_version())]
Remove(RemoveRoleOpt),
/// Show roles currently assigned to nodes and changes staged for commit
#[structopt(name = "show")]
#[structopt(name = "show", version = garage_version())]
Show,
/// Apply staged changes to cluster layout
#[structopt(name = "apply")]
#[structopt(name = "apply", version = garage_version())]
Apply(ApplyLayoutOpt),
/// Revert staged changes to cluster layout
#[structopt(name = "revert")]
#[structopt(name = "revert", version = garage_version())]
Revert(RevertLayoutOpt),
}
@@ -150,43 +151,43 @@ pub struct RevertLayoutOpt {
#[derive(Serialize, Deserialize, StructOpt, Debug)]
pub enum BucketOperation {
/// List buckets
#[structopt(name = "list")]
#[structopt(name = "list", version = garage_version())]
List,
/// Get bucket info
#[structopt(name = "info")]
#[structopt(name = "info", version = garage_version())]
Info(BucketOpt),
/// Create bucket
#[structopt(name = "create")]
#[structopt(name = "create", version = garage_version())]
Create(BucketOpt),
/// Delete bucket
#[structopt(name = "delete")]
#[structopt(name = "delete", version = garage_version())]
Delete(DeleteBucketOpt),
/// Alias bucket under new name
#[structopt(name = "alias")]
#[structopt(name = "alias", version = garage_version())]
Alias(AliasBucketOpt),
/// Remove bucket alias
#[structopt(name = "unalias")]
#[structopt(name = "unalias", version = garage_version())]
Unalias(UnaliasBucketOpt),
/// Allow key to read or write to bucket
#[structopt(name = "allow")]
#[structopt(name = "allow", version = garage_version())]
Allow(PermBucketOpt),
/// Deny key from reading or writing to bucket
#[structopt(name = "deny")]
#[structopt(name = "deny", version = garage_version())]
Deny(PermBucketOpt),
/// Expose as website or not
#[structopt(name = "website")]
#[structopt(name = "website", version = garage_version())]
Website(WebsiteOpt),
/// Set the quotas for this bucket
#[structopt(name = "set-quotas")]
#[structopt(name = "set-quotas", version = garage_version())]
SetQuotas(SetQuotasOpt),
}
@@ -292,35 +293,35 @@ pub struct SetQuotasOpt {
#[derive(Serialize, Deserialize, StructOpt, Debug)]
pub enum KeyOperation {
/// List keys
#[structopt(name = "list")]
#[structopt(name = "list", version = garage_version())]
List,
/// Get key info
#[structopt(name = "info")]
#[structopt(name = "info", version = garage_version())]
Info(KeyOpt),
/// Create new key
#[structopt(name = "new")]
#[structopt(name = "new", version = garage_version())]
New(KeyNewOpt),
/// Rename key
#[structopt(name = "rename")]
#[structopt(name = "rename", version = garage_version())]
Rename(KeyRenameOpt),
/// Delete key
#[structopt(name = "delete")]
#[structopt(name = "delete", version = garage_version())]
Delete(KeyDeleteOpt),
/// Set permission flags for key
#[structopt(name = "allow")]
#[structopt(name = "allow", version = garage_version())]
Allow(KeyPermOpt),
/// Unset permission flags for key
#[structopt(name = "deny")]
#[structopt(name = "deny", version = garage_version())]
Deny(KeyPermOpt),
/// Import key
#[structopt(name = "import")]
#[structopt(name = "import", version = garage_version())]
Import(KeyImportOpt),
}
@@ -392,7 +393,7 @@ pub struct MigrateOpt {
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum MigrateWhat {
/// Migrate buckets and permissions from v0.5.0
#[structopt(name = "buckets050")]
#[structopt(name = "buckets050", version = garage_version())]
Buckets050,
}
@@ -413,19 +414,19 @@ pub struct RepairOpt {
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum RepairWhat {
/// Only do a full sync of metadata tables
#[structopt(name = "tables")]
#[structopt(name = "tables", version = garage_version())]
Tables,
/// Only repair (resync/rebalance) the set of stored blocks
#[structopt(name = "blocks")]
#[structopt(name = "blocks", version = garage_version())]
Blocks,
/// Only redo the propagation of object deletions to the version table (slow)
#[structopt(name = "versions")]
#[structopt(name = "versions", version = garage_version())]
Versions,
/// Only redo the propagation of version deletions to the block ref table (extremely slow)
#[structopt(name = "block_refs")]
#[structopt(name = "block_refs", version = garage_version())]
BlockRefs,
/// Verify integrity of all blocks on disc (extremely slow, i/o intensive)
#[structopt(name = "scrub")]
#[structopt(name = "scrub", version = garage_version())]
Scrub {
#[structopt(subcommand)]
cmd: ScrubCmd,
@@ -435,19 +436,19 @@ pub enum RepairWhat {
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum ScrubCmd {
/// Start scrub
#[structopt(name = "start")]
#[structopt(name = "start", version = garage_version())]
Start,
/// Pause scrub (it will resume automatically after 24 hours)
#[structopt(name = "pause")]
#[structopt(name = "pause", version = garage_version())]
Pause,
/// Resume paused scrub
#[structopt(name = "resume")]
#[structopt(name = "resume", version = garage_version())]
Resume,
/// Cancel scrub in progress
#[structopt(name = "cancel")]
#[structopt(name = "cancel", version = garage_version())]
Cancel,
/// Set tranquility level for in-progress and future scrubs
#[structopt(name = "set-tranquility")]
#[structopt(name = "set-tranquility", version = garage_version())]
SetTranquility {
#[structopt()]
tranquility: u32,
@@ -468,10 +469,10 @@ pub struct OfflineRepairOpt {
pub enum OfflineRepairWhat {
/// Repair K2V item counters
#[cfg(feature = "k2v")]
#[structopt(name = "k2v_item_counters")]
#[structopt(name = "k2v_item_counters", version = garage_version())]
K2VItemCounters,
/// Repair object counters
#[structopt(name = "object_counters")]
#[structopt(name = "object_counters", version = garage_version())]
ObjectCounters,
}
@@ -495,11 +496,17 @@ pub struct WorkerOpt {
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum WorkerCmd {
/// List all workers on Garage node
#[structopt(name = "list")]
#[structopt(name = "list", version = garage_version())]
List {
#[structopt(flatten)]
opt: WorkerListOpt,
},
/// Set worker parameter
#[structopt(name = "set", version = garage_version())]
Set {
#[structopt(subcommand)]
opt: WorkerSetCmd,
},
}
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone, Copy)]
@@ -511,3 +518,16 @@ pub struct WorkerListOpt {
#[structopt(short = "e", long = "errors")]
pub errors: bool,
}
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
pub enum WorkerSetCmd {
/// Set tranquility of scrub operations
#[structopt(name = "scrub-tranquility", version = garage_version())]
ScrubTranquility { tranquility: u32 },
/// Set number of concurrent block resync workers
#[structopt(name = "resync-n-workers", version = garage_version())]
ResyncNWorkers { n_workers: usize },
/// Set tranquility of block resync operations
#[structopt(name = "resync-tranquility", version = garage_version())]
ResyncTranquility { tranquility: u32 },
}
+58 -4
View File
@@ -8,8 +8,15 @@ mod admin;
mod cli;
mod repair;
mod server;
#[cfg(feature = "telemetry-otlp")]
mod tracing_setup;
#[cfg(not(any(feature = "bundled-libs", feature = "system-libs")))]
compile_error!("Either bundled-libs or system-libs Cargo feature must be enabled");
#[cfg(all(feature = "bundled-libs", feature = "system-libs"))]
compile_error!("Only one of bundled-libs and system-libs Cargo features must be enabled");
use std::net::SocketAddr;
use std::path::PathBuf;
@@ -29,7 +36,10 @@ use admin::*;
use cli::*;
#[derive(StructOpt, Debug)]
#[structopt(name = "garage")]
#[structopt(
name = "garage",
about = "S3-compatible object store for self-hosted geo-distributed deployments"
)]
struct Opt {
/// Host to connect to for admin operations, in the format:
/// <public-key>@<ip>:<port>
@@ -58,7 +68,10 @@ async fn main() {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "netapp=info,garage=info")
}
pretty_env_logger::init();
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
.init();
sodiumoxide::init().expect("Unable to init sodiumoxide");
// Abort on panic (same behavior as in Go)
@@ -67,7 +80,42 @@ async fn main() {
std::process::abort();
}));
let opt = Opt::from_args();
// Initialize version and features info
let features = &[
#[cfg(feature = "k2v")]
"k2v",
#[cfg(feature = "sled")]
"sled",
#[cfg(feature = "lmdb")]
"lmdb",
#[cfg(feature = "sqlite")]
"sqlite",
#[cfg(feature = "consul-discovery")]
"consul-discovery",
#[cfg(feature = "kubernetes-discovery")]
"kubernetes-discovery",
#[cfg(feature = "metrics")]
"metrics",
#[cfg(feature = "telemetry-otlp")]
"telemetry-otlp",
#[cfg(feature = "bundled-libs")]
"bundled-libs",
#[cfg(feature = "system-libs")]
"system-libs",
][..];
if let Some(git_version) = option_env!("GIT_VERSION") {
garage_util::version::init_version(git_version);
}
garage_util::version::init_features(features);
// Parse arguments
let version = format!(
"{} [features: {}]",
garage_util::version::garage_version(),
features.join(", ")
);
let opt = Opt::from_clap(&Opt::clap().version(version.as_str()).get_matches());
let res = match opt.cmd {
Command::Server => server::run_server(opt.config_file).await,
Command::OfflineRepair(repair_opt) => {
@@ -116,7 +164,13 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
} else {
let node_id = garage_rpc::system::read_node_id(&config.as_ref().unwrap().metadata_dir)
.err_context(READ_KEY_ERROR)?;
if let Some(a) = config.as_ref().and_then(|c| c.rpc_public_addr) {
if let Some(a) = config.as_ref().and_then(|c| c.rpc_public_addr.as_ref()) {
use std::net::ToSocketAddrs;
let a = a
.to_socket_addrs()
.ok_or_message("unable to resolve rpc_public_addr specified in config file")?
.next()
.ok_or_message("unable to resolve rpc_public_addr specified in config file")?;
(node_id, a)
} else {
let default_addr = SocketAddr::new(
+127 -46
View File
@@ -9,12 +9,13 @@ use garage_util::error::Error;
use garage_api::admin::api_server::AdminApiServer;
use garage_api::s3::api_server::S3ApiServer;
use garage_model::garage::Garage;
use garage_web::run_web_server;
use garage_web::WebServer;
#[cfg(feature = "k2v")]
use garage_api::k2v::api_server::K2VApiServer;
use crate::admin::*;
#[cfg(feature = "telemetry-otlp")]
use crate::tracing_setup::*;
async fn wait_from(mut chan: watch::Receiver<bool>) {
@@ -29,20 +30,34 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
info!("Loading configuration...");
let config = read_config(config_file)?;
// ---- Initialize Garage internals ----
#[cfg(feature = "metrics")]
let metrics_exporter = opentelemetry_prometheus::exporter().init();
info!("Initializing background runner...");
let watch_cancel = netapp::util::watch_ctrl_c();
let watch_cancel = watch_shutdown_signal();
let (background, await_background_done) = BackgroundRunner::new(16, watch_cancel.clone());
info!("Initializing Garage main data store...");
let garage = Garage::new(config.clone(), background)?;
info!("Initialize tracing...");
if let Some(export_to) = config.admin.trace_sink {
init_tracing(&export_to, garage.system.id)?;
if config.admin.trace_sink.is_some() {
info!("Initialize tracing...");
#[cfg(feature = "telemetry-otlp")]
init_tracing(config.admin.trace_sink.as_ref().unwrap(), garage.system.id)?;
#[cfg(not(feature = "telemetry-otlp"))]
error!("Garage was built without OTLP exporter, admin.trace_sink is ignored.");
}
info!("Initialize Admin API server and metrics collector...");
let admin_server = AdminApiServer::new(garage.clone());
let admin_server = AdminApiServer::new(
garage.clone(),
#[cfg(feature = "metrics")]
metrics_exporter,
);
info!("Launching internal Garage cluster communications...");
let run_system = tokio::spawn(garage.system.clone().run(watch_cancel.clone()));
@@ -50,53 +65,78 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
info!("Create admin RPC handler...");
AdminRpcHandler::new(garage.clone());
info!("Initializing S3 API server...");
let s3_api_server = tokio::spawn(S3ApiServer::run(
garage.clone(),
wait_from(watch_cancel.clone()),
));
// ---- Launch public-facing API servers ----
#[cfg(feature = "k2v")]
let k2v_api_server = {
info!("Initializing K2V API server...");
tokio::spawn(K2VApiServer::run(
garage.clone(),
wait_from(watch_cancel.clone()),
))
};
let mut servers = vec![];
info!("Initializing web server...");
let web_server = tokio::spawn(run_web_server(
garage.clone(),
wait_from(watch_cancel.clone()),
));
if let Some(s3_bind_addr) = &config.s3_api.api_bind_addr {
info!("Initializing S3 API server...");
servers.push((
"S3 API",
tokio::spawn(S3ApiServer::run(
garage.clone(),
*s3_bind_addr,
config.s3_api.s3_region.clone(),
wait_from(watch_cancel.clone()),
)),
));
}
info!("Launching Admin API server...");
let admin_server = tokio::spawn(admin_server.run(wait_from(watch_cancel.clone())));
if config.k2v_api.is_some() {
#[cfg(feature = "k2v")]
{
info!("Initializing K2V API server...");
servers.push((
"K2V API",
tokio::spawn(K2VApiServer::run(
garage.clone(),
config.k2v_api.as_ref().unwrap().api_bind_addr,
config.s3_api.s3_region.clone(),
wait_from(watch_cancel.clone()),
)),
));
}
#[cfg(not(feature = "k2v"))]
error!("K2V is not enabled in this build, cannot start K2V API server");
}
if let Some(web_config) = &config.s3_web {
info!("Initializing web server...");
servers.push((
"Web",
tokio::spawn(WebServer::run(
garage.clone(),
web_config.bind_addr,
web_config.root_domain.clone(),
wait_from(watch_cancel.clone()),
)),
));
}
if let Some(admin_bind_addr) = &config.admin.api_bind_addr {
info!("Launching Admin API server...");
servers.push((
"Admin",
tokio::spawn(admin_server.run(*admin_bind_addr, wait_from(watch_cancel.clone()))),
));
}
#[cfg(not(feature = "metrics"))]
if config.admin.metrics_token.is_some() {
warn!("This Garage version is built without the metrics feature");
}
// Stuff runs
// When a cancel signal is sent, stuff stops
if let Err(e) = s3_api_server.await? {
warn!("S3 API server exited with error: {}", e);
} else {
info!("S3 API server exited without error.");
}
#[cfg(feature = "k2v")]
if let Err(e) = k2v_api_server.await? {
warn!("K2V API server exited with error: {}", e);
} else {
info!("K2V API server exited without error.");
}
if let Err(e) = web_server.await? {
warn!("Web server exited with error: {}", e);
} else {
info!("Web server exited without error.");
}
if let Err(e) = admin_server.await? {
warn!("Admin web server exited with error: {}", e);
} else {
info!("Admin API server exited without error.");
// Collect stuff
for (desc, join_handle) in servers {
if let Err(e) = join_handle.await? {
error!("{} server exited with error: {}", desc, e);
} else {
info!("{} server exited without error.", desc);
}
}
// Remove RPC handlers for system to break reference cycles
@@ -117,3 +157,44 @@ pub async fn run_server(config_file: PathBuf) -> Result<(), Error> {
Ok(())
}
#[cfg(unix)]
fn watch_shutdown_signal() -> watch::Receiver<bool> {
use tokio::signal::unix::*;
let (send_cancel, watch_cancel) = watch::channel(false);
tokio::spawn(async move {
let mut sigint = signal(SignalKind::interrupt()).expect("Failed to install SIGINT handler");
let mut sigterm =
signal(SignalKind::terminate()).expect("Failed to install SIGTERM handler");
let mut sighup = signal(SignalKind::hangup()).expect("Failed to install SIGHUP handler");
tokio::select! {
_ = sigint.recv() => info!("Received SIGINT, shutting down."),
_ = sigterm.recv() => info!("Received SIGTERM, shutting down."),
_ = sighup.recv() => info!("Received SIGHUP, shutting down."),
}
send_cancel.send(true).unwrap();
});
watch_cancel
}
#[cfg(windows)]
fn watch_shutdown_signal() -> watch::Receiver<bool> {
use tokio::signal::windows::*;
let (send_cancel, watch_cancel) = watch::channel(false);
tokio::spawn(async move {
let mut sigint = ctrl_c().expect("Failed to install Ctrl-C handler");
let mut sigclose = ctrl_close().expect("Failed to install Ctrl-Close handler");
let mut siglogoff = ctrl_logoff().expect("Failed to install Ctrl-Logoff handler");
let mut sigsdown = ctrl_shutdown().expect("Failed to install Ctrl-Shutdown handler");
tokio::select! {
_ = sigint.recv() => info!("Received Ctrl-C, shutting down."),
_ = sigclose.recv() => info!("Received Ctrl-Close, shutting down."),
_ = siglogoff.recv() => info!("Received Ctrl-Logoff, shutting down."),
_ = sigsdown.recv() => info!("Received Ctrl-Shutdown, shutting down."),
}
send_cancel.send(true).unwrap();
});
watch_cancel
}
+11 -11
View File
@@ -6,7 +6,7 @@ use assert_json_diff::assert_json_eq;
use serde_json::json;
use super::json_body;
use hyper::Method;
use hyper::{Method, StatusCode};
#[tokio::test]
async fn test_batch() {
@@ -49,7 +49,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
for sk in ["a", "b", "c", "d.1", "d.2", "e"] {
let res = ctx
@@ -62,7 +62,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -104,7 +104,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
let json_res = json_body(res).await;
assert_json_eq!(
json_res,
@@ -266,7 +266,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
for sk in ["b", "c", "d.1", "d.2"] {
let res = ctx
@@ -280,9 +280,9 @@ async fn test_batch() {
.await
.unwrap();
if sk == "b" {
assert_eq!(res.status(), 204);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
} else {
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
}
ct.insert(
sk,
@@ -317,7 +317,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
let json_res = json_body(res).await;
assert_json_eq!(
json_res,
@@ -478,7 +478,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
let json_res = json_body(res).await;
assert_json_eq!(
json_res,
@@ -514,7 +514,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -547,7 +547,7 @@ async fn test_batch() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
let json_res = json_body(res).await;
assert_json_eq!(
json_res,
+10 -10
View File
@@ -1,13 +1,13 @@
use crate::common;
use hyper::Method;
use hyper::{Method, StatusCode};
#[tokio::test]
async fn test_error_codes() {
let ctx = common::context();
let bucket = ctx.create_bucket("test-k2v-error-codes");
// Regular insert should work (code 200)
// Regular insert should work (code 204)
let res = ctx
.k2v
.request
@@ -19,7 +19,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Insert with trash causality token: invalid request
let res = ctx
@@ -34,7 +34,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Search without partition key: invalid request
let res = ctx
@@ -52,7 +52,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Search with start that is not in prefix: invalid request
let res = ctx
@@ -70,7 +70,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Search with invalid json: 400
let res = ctx
@@ -88,7 +88,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Batch insert with invalid causality token: 400
let res = ctx
@@ -105,7 +105,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Batch insert with invalid data: 400
let res = ctx
@@ -122,7 +122,7 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Poll with invalid causality token: 400
let res = ctx
@@ -137,5 +137,5 @@ async fn test_error_codes() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
+29 -29
View File
@@ -6,7 +6,7 @@ use assert_json_diff::assert_json_eq;
use serde_json::json;
use super::json_body;
use hyper::Method;
use hyper::{Method, StatusCode};
#[tokio::test]
async fn test_items_and_indices() {
@@ -56,7 +56,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Get value back
let res = ctx
@@ -69,7 +69,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -132,7 +132,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Get value back
let res = ctx
@@ -145,7 +145,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -201,7 +201,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Get value back
let res = ctx
@@ -214,7 +214,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -271,7 +271,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
let ct = res
.headers()
.get("x-garage-causality-token")
@@ -292,7 +292,7 @@ async fn test_items_and_indices() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// ReadIndex -- now there should be some stuff
tokio::time::sleep(Duration::from_secs(1)).await;
@@ -364,7 +364,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// f0: either
let res = ctx
@@ -377,7 +377,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -405,7 +405,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -424,7 +424,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/octet-stream"
@@ -446,7 +446,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -466,7 +466,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// f0: either
let res = ctx
@@ -479,7 +479,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -503,7 +503,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -528,7 +528,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 409); // CONFLICT
assert_eq!(res.status(), StatusCode::CONFLICT); // CONFLICT
// f3: json
let res = ctx
@@ -541,7 +541,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -568,7 +568,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// f0: either
let res = ctx
@@ -581,7 +581,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -599,7 +599,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -625,7 +625,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 409); // CONFLICT
assert_eq!(res.status(), StatusCode::CONFLICT); // CONFLICT
// f3: json
let res = ctx
@@ -638,7 +638,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -658,7 +658,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// f0: either
let res = ctx
@@ -671,7 +671,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204); // NO CONTENT
assert_eq!(res.status(), StatusCode::NO_CONTENT); // NO CONTENT
// f1: not specified
let res = ctx
@@ -683,7 +683,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
@@ -702,7 +702,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 204); // NO CONTENT
assert_eq!(res.status(), StatusCode::NO_CONTENT); // NO CONTENT
// f3: json
let res = ctx
@@ -715,7 +715,7 @@ async fn test_item_return_format() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
+5 -5
View File
@@ -1,4 +1,4 @@
use hyper::Method;
use hyper::{Method, StatusCode};
use std::time::Duration;
use crate::common;
@@ -20,7 +20,7 @@ async fn test_poll() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Retrieve initial value to get its causality token
let res2 = ctx
@@ -33,7 +33,7 @@ async fn test_poll() {
.send()
.await
.unwrap();
assert_eq!(res2.status(), 200);
assert_eq!(res2.status(), StatusCode::OK);
let ct = res2
.headers()
.get("x-garage-causality-token")
@@ -80,7 +80,7 @@ async fn test_poll() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// Check poll finishes with correct value
let poll_res = tokio::select! {
@@ -88,7 +88,7 @@ async fn test_poll() {
res = poll => res.unwrap().unwrap(),
};
assert_eq!(poll_res.status(), 200);
assert_eq!(poll_res.status(), StatusCode::OK);
let poll_res_body = hyper::body::to_bytes(poll_res.into_body())
.await
+3 -3
View File
@@ -1,6 +1,6 @@
use crate::common;
use hyper::Method;
use hyper::{Method, StatusCode};
#[tokio::test]
async fn test_simple() {
@@ -18,7 +18,7 @@ async fn test_simple() {
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
assert_eq!(res.status(), StatusCode::NO_CONTENT);
let res2 = ctx
.k2v
@@ -30,7 +30,7 @@ async fn test_simple() {
.send()
.await
.unwrap();
assert_eq!(res2.status(), 200);
assert_eq!(res2.status(), StatusCode::OK);
let res2_body = hyper::body::to_bytes(res2.into_body())
.await
+4 -1
View File
@@ -3,5 +3,8 @@ mod common;
mod admin;
mod bucket;
mod k2v;
mod s3;
#[cfg(feature = "k2v")]
mod k2v;
+9
View File
@@ -263,4 +263,13 @@ async fn test_deleteobject() {
.unwrap();
assert!(l.contents.is_none());
// Deleting a non-existing object shouldn't be a problem
ctx.client
.delete_object()
.bucket(&bucket)
.key("l-0")
.send()
.await
.unwrap();
}
+10 -10
View File
@@ -4,7 +4,7 @@ use aws_sdk_s3::{
model::{CorsConfiguration, CorsRule, ErrorDocument, IndexDocument, WebsiteConfiguration},
types::ByteStream,
};
use http::Request;
use http::{Request, StatusCode};
use hyper::{
body::{to_bytes, Body},
Client,
@@ -43,7 +43,7 @@ async fn test_website() {
let mut resp = client.request(req()).await.unwrap();
assert_eq!(resp.status(), 404);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert_ne!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
@@ -56,7 +56,7 @@ async fn test_website() {
.expect_success_status("Could not allow website on bucket");
resp = client.request(req()).await.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
@@ -69,7 +69,7 @@ async fn test_website() {
.expect_success_status("Could not deny website on bucket");
resp = client.request(req()).await.unwrap();
assert_eq!(resp.status(), 404);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert_ne!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
@@ -175,7 +175,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get("access-control-allow-origin").unwrap(),
"*"
@@ -200,7 +200,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 404);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert_eq!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY_ERR.as_ref()
@@ -220,7 +220,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get("access-control-allow-origin").unwrap(),
"*"
@@ -244,7 +244,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 403);
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
assert_ne!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
@@ -285,7 +285,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 403);
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
assert_ne!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref()
@@ -311,7 +311,7 @@ async fn test_website_s3_api() {
let mut resp = client.request(req).await.unwrap();
assert_eq!(resp.status(), 404);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert_ne!(
to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY_ERR.as_ref()
+2 -2
View File
@@ -12,7 +12,7 @@ readme = "../../README.md"
base64 = "0.13.0"
http = "0.2.6"
log = "0.4"
rusoto_core = "0.48.0"
rusoto_core = { version = "0.48.0", default-features = false, features = ["rustls"] }
rusoto_credential = "0.48.0"
rusoto_signature = "0.48.0"
serde = "1.0.137"
@@ -22,7 +22,7 @@ tokio = "1.17.0"
# cli deps
clap = { version = "3.1.18", optional = true, features = ["derive", "env"] }
garage_util = { version = "0.7.0", path = "../util", optional = true }
garage_util = { version = "0.8.0", path = "../util", optional = true }
[features]
+9 -9
View File
@@ -1,6 +1,6 @@
[package]
name = "garage_model"
version = "0.7.0"
version = "0.8.0"
authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018"
license = "AGPL-3.0"
@@ -15,11 +15,10 @@ path = "lib.rs"
[dependencies]
garage_db = { version = "0.8.0", path = "../db" }
garage_rpc = { version = "0.7.0", path = "../rpc" }
garage_table = { version = "0.7.0", path = "../table" }
garage_block = { version = "0.7.0", path = "../block" }
garage_util = { version = "0.7.0", path = "../util" }
garage_model_050 = { package = "garage_model", version = "0.5.1" }
garage_rpc = { version = "0.8.0", path = "../rpc" }
garage_table = { version = "0.8.0", path = "../table" }
garage_block = { version = "0.8.0", path = "../block" }
garage_util = { version = "0.8.0", path = "../util" }
async-trait = "0.1.7"
arc-swap = "1.0"
@@ -40,9 +39,10 @@ futures-util = "0.3"
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
opentelemetry = "0.17"
#netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" }
#netapp = { version = "0.4", path = "../../../netapp" }
netapp = "0.4"
netapp = "0.5"
[features]
k2v = [ "garage_util/k2v" ]
lmdb = [ "garage_db/lmdb" ]
sled = [ "garage_db/sled" ]
sqlite = [ "garage_db/sqlite" ]
+1 -1
View File
@@ -7,7 +7,7 @@ use garage_table::*;
/// The bucket alias table holds the names given to buckets
/// in the global namespace.
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct BucketAlias {
name: String,
pub state: crdt::Lww<Option<Uuid>>,
+2 -2
View File
@@ -12,7 +12,7 @@ use crate::permission::BucketKeyPerm;
/// Its parameters are not directly accessible as:
/// - It must be possible to merge paramaters, hence the use of a LWW CRDT.
/// - A bucket has 2 states, Present or Deleted and parameters make sense only if present.
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct Bucket {
/// ID of the bucket
pub id: Uuid,
@@ -21,7 +21,7 @@ pub struct Bucket {
}
/// Configuration for a bucket
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct BucketParams {
/// Bucket's creation date
pub creation_date: u64,
+46 -11
View File
@@ -6,7 +6,7 @@ use garage_db as db;
use garage_util::background::*;
use garage_util::config::*;
use garage_util::error::Error;
use garage_util::error::*;
use garage_rpc::system::System;
@@ -76,10 +76,17 @@ pub struct GarageK2V {
impl Garage {
/// Create and run garage
pub fn new(config: Config, background: Arc<BackgroundRunner>) -> Result<Arc<Self>, Error> {
// Create meta dir and data dir if they don't exist already
std::fs::create_dir_all(&config.metadata_dir)
.ok_or_message("Unable to create Garage metadata directory")?;
std::fs::create_dir_all(&config.data_dir)
.ok_or_message("Unable to create Garage data directory")?;
info!("Opening database...");
let mut db_path = config.metadata_dir.clone();
std::fs::create_dir_all(&db_path).expect("Unable to create Garage meta data directory");
let db = match config.db_engine.as_str() {
// ---- Sled DB ----
#[cfg(feature = "sled")]
"sled" => {
db_path.push("db");
info!("Opening Sled database at: {}", db_path.display());
@@ -91,6 +98,10 @@ impl Garage {
.expect("Unable to open sled DB");
db::sled_adapter::SledDb::init(db)
}
#[cfg(not(feature = "sled"))]
"sled" => return Err(Error::Message("sled db not available in this build".into())),
// ---- Sqlite DB ----
#[cfg(feature = "sqlite")]
"sqlite" | "sqlite3" | "rusqlite" => {
db_path.push("db.sqlite");
info!("Opening Sqlite database at: {}", db_path.display());
@@ -98,23 +109,48 @@ impl Garage {
.expect("Unable to open sqlite DB");
db::sqlite_adapter::SqliteDb::init(db)
}
#[cfg(not(feature = "sqlite"))]
"sqlite" | "sqlite3" | "rusqlite" => {
return Err(Error::Message(
"sqlite db not available in this build".into(),
))
}
// ---- LMDB DB ----
#[cfg(feature = "lmdb")]
"lmdb" | "heed" => {
db_path.push("db.lmdb");
info!("Opening LMDB database at: {}", db_path.display());
std::fs::create_dir_all(&db_path).expect("Unable to create LMDB data directory");
let map_size = garage_db::lmdb_adapter::recommended_map_size();
let db = db::lmdb_adapter::heed::EnvOpenOptions::new()
.max_dbs(100)
.map_size(map_size)
.open(&db_path)
.expect("Unable to open LMDB DB");
use db::lmdb_adapter::heed;
let mut env_builder = heed::EnvOpenOptions::new();
env_builder.max_dbs(100);
env_builder.max_readers(500);
env_builder.map_size(map_size);
unsafe {
env_builder.flag(heed::flags::Flags::MdbNoSync);
env_builder.flag(heed::flags::Flags::MdbNoMetaSync);
}
let db = env_builder.open(&db_path).expect("Unable to open LMDB DB");
db::lmdb_adapter::LmdbDb::init(db)
}
#[cfg(not(feature = "lmdb"))]
"lmdb" | "heed" => return Err(Error::Message("lmdb db not available in this build".into())),
// ---- Unavailable DB engine ----
e => {
return Err(Error::Message(format!(
"Unsupported DB engine: {} (options: sled, sqlite, lmdb)",
e
"Unsupported DB engine: {} (options: {})",
e,
vec![
#[cfg(feature = "sled")]
"sled",
#[cfg(feature = "sqlite")]
"sqlite",
#[cfg(feature = "lmdb")]
"lmdb",
]
.join(", ")
)));
}
};
@@ -133,7 +169,7 @@ impl Garage {
background.clone(),
replication_mode.replication_factor(),
&config,
);
)?;
let data_rep_param = TableShardedReplication {
system: system.clone(),
@@ -159,7 +195,6 @@ impl Garage {
&db,
config.data_dir.clone(),
config.compression_level,
config.block_manager_background_tranquility,
data_rep_param,
system.clone(),
);
+1 -1
View File
@@ -81,7 +81,7 @@ impl<T: CountedItem> CounterEntry<T> {
}
/// A counter entry in the global table
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct CounterValue {
pub node_values: BTreeMap<Uuid, (u64, i64)>,
}
+1 -1
View File
@@ -15,7 +15,7 @@ pub fn make_node_id(node_id: Uuid) -> K2VNodeId {
u64::from_be_bytes(tmp)
}
#[derive(PartialEq, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct CausalContext {
pub vector_clock: BTreeMap<K2VNodeId, u64>,
}
+4 -4
View File
@@ -17,7 +17,7 @@ pub const CONFLICTS: &str = "conflicts";
pub const VALUES: &str = "values";
pub const BYTES: &str = "bytes";
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct K2VItem {
pub partition: K2VItemPartition,
pub sort_key: String,
@@ -25,19 +25,19 @@ pub struct K2VItem {
items: BTreeMap<K2VNodeId, DvvsEntry>,
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash, Eq)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)]
pub struct K2VItemPartition {
pub bucket_id: Uuid,
pub partition_key: String,
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
struct DvvsEntry {
t_discard: u64,
values: Vec<(u64, DvvsValue)>,
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub enum DvvsValue {
Value(#[serde(with = "serde_bytes")] Vec<u8>),
Deleted,
+17 -19
View File
@@ -23,7 +23,6 @@ use garage_rpc::system::System;
use garage_rpc::*;
use garage_table::replication::{TableReplication, TableShardedReplication};
use garage_table::table::TABLE_RPC_TIMEOUT;
use garage_table::{PartitionKey, Table};
use crate::k2v::causality::*;
@@ -117,7 +116,6 @@ impl K2VRpcHandler {
}),
RequestStrategy::with_priority(PRIO_NORMAL)
.with_quorum(1)
.with_timeout(TABLE_RPC_TIMEOUT)
.interrupt_after_quorum(true),
)
.await?;
@@ -169,7 +167,6 @@ impl K2VRpcHandler {
K2VRpc::InsertManyItems(items),
RequestStrategy::with_priority(PRIO_NORMAL)
.with_quorum(1)
.with_timeout(TABLE_RPC_TIMEOUT)
.interrupt_after_quorum(true),
)
.await?;
@@ -205,22 +202,23 @@ impl K2VRpcHandler {
.replication
.write_nodes(&poll_key.partition.hash());
let resps = self
.system
.rpc
.try_call_many(
&self.endpoint,
&nodes[..],
K2VRpc::PollItem {
key: poll_key,
causal_context,
timeout_msec,
},
RequestStrategy::with_priority(PRIO_NORMAL)
.with_quorum(self.item_table.data.replication.read_quorum())
.with_timeout(Duration::from_millis(timeout_msec) + TABLE_RPC_TIMEOUT),
)
.await?;
let rpc = self.system.rpc.try_call_many(
&self.endpoint,
&nodes[..],
K2VRpc::PollItem {
key: poll_key,
causal_context,
timeout_msec,
},
RequestStrategy::with_priority(PRIO_NORMAL)
.with_quorum(self.item_table.data.replication.read_quorum())
.without_timeout(),
);
let timeout_duration = Duration::from_millis(timeout_msec) + self.system.rpc.rpc_timeout();
let resps = select! {
r = rpc => r?,
_ = tokio::time::sleep(timeout_duration) => return Ok(None),
};
let mut resp: Option<K2VItem> = None;
for v in resps {
+3 -3
View File
@@ -6,10 +6,10 @@ use garage_util::data::*;
use crate::permission::BucketKeyPerm;
use garage_model_050::key_table as old;
use crate::prev::v051::key_table as old;
/// An api key
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct Key {
/// The id of the key (immutable), used as partition key
pub key_id: String,
@@ -19,7 +19,7 @@ pub struct Key {
}
/// Configuration for a key
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct KeyParams {
/// The secret_key associated (immutable)
pub secret_key: String,
+3
View File
@@ -1,6 +1,9 @@
#[macro_use]
extern crate tracing;
// For migration from previous versions
pub(crate) mod prev;
pub mod permission;
pub mod index_counter;

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