Compare commits

...

500 Commits

Author SHA1 Message Date
Alex Auvolat b7a153b892 CI: fix use of aws cp to talk to older Garage versions
chicken and egg problem
2025-05-23 19:06:58 +02:00
Alex Auvolat bc8e6af223 update admin API documentation 2025-05-23 17:25:17 +02:00
Alex Auvolat 78b1481461 update version numbers in helm chart 2025-05-23 16:43:14 +02:00
Alex Auvolat 7ab1d176d4 Merge branch 'main' into next-v2 2025-05-23 16:33:07 +02:00
Alex a8d73682a4 Merge pull request 'more resilience to inconsistent alias states' (#989) from fix-bucket-aliases into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/989
2025-05-22 17:41:42 +00:00
Alex Auvolat 8654eb19bf implement repair procedure to fix inconsistent bucket aliases 2025-05-22 19:34:38 +02:00
maximilien 54ea412188 Merge pull request 'Add kubernetes CRD' (#994) from babykart/garage:k8s-crd into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/994
2025-05-22 17:15:56 +00:00
Alex Auvolat 2ade8c86f6 more resilience to inconsistent alias states 2025-05-22 19:12:05 +02:00
babykart b15e2cbb6c Update Kubernetes cookbook
Signed-off-by: babykart <babykart@gmail.com>
2025-05-22 17:11:14 +00:00
babykart 0fd1b7342b Add Kubernetes CRD and the related kustomization
Signed-off-by: babykart <babykart@gmail.com>
2025-05-22 17:11:14 +00:00
Alex b15d55ea9f Merge pull request 'implement x-amz-checksum-type' (#1025) from checksum-type into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1025
2025-05-22 17:06:04 +00:00
Alex Auvolat c13af97b81 return ChecksumType in CompleteMultipartUpload result 2025-05-22 18:24:29 +02:00
Alex Auvolat d1d5c67ba7 fix test-smoke for CompleteMultipartUpload 2025-05-22 18:14:53 +02:00
Alex Auvolat 77125e9464 add boto3 test for STREAMING-UNSIGNED-PAYLOAD-TRAILER 2025-05-22 17:47:12 +02:00
Alex Auvolat cfd10480ee CopyObject: fix checksumming choice logic 2025-05-22 17:07:10 +02:00
Alex Auvolat fbb40c4ea0 object_table: merge checksum_algorithm and checksum_type for Uploading state 2025-05-22 17:07:10 +02:00
Alex Auvolat e475c7f802 api: switch to crc_fast for checksumming and implement full-object mpu checksums 2025-05-22 17:03:54 +02:00
Alex Auvolat 589a992af8 api: parse x-amz-checksum-type and validate combination with algo 2025-05-22 17:03:54 +02:00
Alex Auvolat 768794daae api: change ExpectedChecksums to not be a reference 2025-05-22 17:03:54 +02:00
Alex Auvolat abe0546ab0 model: store x-amz-checksum-type (full_object | composite) 2025-05-22 17:03:54 +02:00
Alex be16bc7a05 Merge pull request 'Fix behavior of CopyObject wrt x-amz-website-redirect-location' (#1037) from Armael/garage:copy-website-redirect into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1037
2025-05-22 13:57:28 +00:00
Alex bfaa1ca6b7 Merge pull request 'api: lifecycle: 404 if missing lifecycle config' (#1043) from ragazenta/garage:no-lifecycle-response into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1043
2025-05-22 13:56:52 +00:00
Alex de8eeab4ad Merge pull request 'optionally support puny code (fix #273)' (#1042) from trinity-1686a/garage:1686a/punnycode into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1042
2025-05-22 12:49:10 +00:00
Renjaya Raga Zenta ae3f7ee76c api: lifecycle: 404 if missing lifecycle config 2025-05-22 19:33:54 +07:00
trinity-1686a 2dc3a6dbbe document allow_punycode configuration option 2025-05-22 14:08:06 +02:00
Armaël Guéneau c6bc3f229b Fix behavior of CopyObject wrt x-amz-website-redirect-location 2025-05-22 14:03:11 +02:00
Alex 47fe96279b Merge pull request 'throw error on legacy replication_mode setting' (#1018) from yuka/garage:remove-replication-mode into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1018
2025-05-22 10:57:24 +00:00
trinity-1686a bba9202f31 add test for punycode 2025-05-19 20:36:03 +02:00
trinity-1686a a605a80806 support punnycode in api/web endpoint 2025-05-19 18:11:55 +02:00
trinity-1686a 539af12d21 allow punnycode in bucket name 2025-05-19 18:07:04 +02:00
Yureka 45bdf54e7e throw error on legacy replication_mode setting 2025-05-15 08:09:56 +02:00
Alex a4b431163c Merge pull request 'speed up UploadPartCopy' (#1034) from yuka/garage:uploadpartcopy into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1034
2025-05-14 20:31:43 +00:00
Yureka db54bf96c7 speed up UploadPartCopy 2025-05-14 02:12:41 +02:00
Alex a2a9e3cec4 Merge pull request 'doc: Add systemd example to increase file descriptors limit' (#1023) from baptiste/garage:systemd_openfiles into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1023
2025-05-09 10:34:07 +00:00
Baptiste Jonglez 14274bc13c doc: Add systemd example to increase file descriptors limit 2025-05-08 10:27:53 +02:00
Yureka cbcdab4e24 scripts: replication_mode -> replication_factor+consistency_mode 2025-05-04 16:43:56 +02:00
Alex 38ca35eb0f Merge pull request 'refactor: make TableShardedReplication a thin wrapper around LayoutManager' (#820) from yuka/garage:refactor-sharded-table into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/820
2025-04-28 10:43:35 +00:00
Yureka a2d87a012d refactor: use replication factor of the layout versions in calculate_sync_map_min_with_quorum 2025-04-28 11:51:01 +02:00
Yureka 899292ee28 refactor: make TableShardedReplication a thin wrapper around LayoutManager 2025-04-28 11:51:01 +02:00
Alex bf4691d98a Merge pull request 'Fix #1007: hint that region can be changed depending on cluster config' (#1015) from garage-1007-update-region-in-doc into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1015
2025-04-24 07:41:32 +00:00
Maximilien R. ad151cb1dc Fix #1007: hint that region can be changed depending on cluster config 2025-04-23 23:30:16 +02:00
Yureka c8e9c45889 refactor: Use ReplicationFactor type in more places
- Remove the replication_factor.replication_factor() in favor of
  usize::from(replication_factor) to make the conversion more explicit.

- Implement Display on ReplicationFactor so that it can be formatted
  without converting to usize

- Use ReplicationFactor in the constructor of LayoutVersion and add a
  method to get a ReplicationFactor from a LayoutVersion, despite
  LayoutVersion still storing it as usize internally.
2025-04-21 19:47:14 +02:00
babykart 3c20984a08 helm-chart: Cosmetic changes
Signed-off-by: babykart <babykart@gmail.com>
2025-04-21 10:04:53 +00:00
babykart e6e4e051a1 helm-chart: Add metadata_auto_snapshot_interval
Signed-off-by: babykart <babykart@gmail.com>
2025-04-21 10:04:53 +00:00
babykart 9b38cba6f3 helm-chart: Add livenessProbe & readinessProbe
Signed-off-by: babykart <babykart@gmail.com>
2025-04-21 10:04:53 +00:00
Alex 4ef954d176 Merge pull request 'Fix Docker run volume mappings' (#1012) from Zoob/garage:main into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1012
2025-04-19 20:05:16 +00:00
Zoob 02498a93d0 doc: fix Docker run volume mappings 2025-04-19 18:46:36 +00:00
Alex 4caad5425d Merge pull request 'metadata: Create compact LMDB snapshots' (#1008) from baptiste/garage:lmdb_compact_snapshot into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1008
2025-04-17 19:26:32 +00:00
Alex Auvolat e79b485aa8 fix panic in ListAdminTokens 2025-04-17 17:38:20 +02:00
Alex Auvolat d38d62f4d7 bump version to v2.0.0 2025-04-17 12:36:41 +02:00
Alex 2885806e00 Merge pull request 'creation and expiration dates' (#1010) from key-creation-expiration into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1010
2025-04-17 10:23:36 +00:00
Alex Auvolat 52437e4298 refactor parsing of --expires-in 2025-04-17 12:14:51 +02:00
Alex Auvolat abcef7a3fd cli: implement garage key delete-expired 2025-04-17 11:58:19 +02:00
Alex Auvolat 5d338f0b8f add never_expires to remove expiration dates of admin tokens and access keys 2025-04-17 11:44:09 +02:00
Alex Auvolat 590c9bb4db possibility to update access key expiration date 2025-04-17 11:30:58 +02:00
Alex Auvolat c56b7e20c3 add creation date and expiration date to access keys 2025-04-17 11:28:44 +02:00
Alex Auvolat 2f21181ccb publish bucket creation date in admin api and CLI 2025-04-17 10:29:23 +02:00
Baptiste Jonglez 9ec3f8cc3c metadata: Create compact LMDB snapshots
See #1006

LMDB files never shrink, so we can end up with a large database that
contains a smaller amount of actual data.

Compacting the snapshots is an easy win: it will write faster to disk,
take less space, and if needed you can reimport an already-compacted
snapshot as the main database.
2025-04-12 23:18:50 +02:00
Alex 2d1c073d2f Merge pull request 'admin api: implement InspectObject (fix #892)' (#1005) from inspect-object into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1005
2025-04-06 12:33:11 +00:00
Alex Auvolat 5e7307cbf3 admin api: add comments for InspectObject 2025-04-06 14:21:54 +02:00
Alex Auvolat fd0e23e984 admin api: implement InspectObject (fix #892) 2025-04-06 13:23:39 +02:00
Alex d7506b282c Merge pull request 'admin api: always return latest bucket info' (#1004) from fix-bucket-info-api into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1004
2025-04-06 09:43:41 +00:00
Alex Auvolat 6bbdca2e48 admin api: always return latest bucket info 2025-04-06 11:14:42 +02:00
Alex c6d6cc1fc3 Merge pull request 'read/write quorums on admin operations' (#997) from admin-quorums into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/997
2025-03-25 16:09:06 +00:00
Alex Auvolat 5fa6df6ee3 improve comments in bucket helper 2025-03-25 16:58:12 +01:00
Alex Auvolat c6bed26347 relocalize logic into block manager 2025-03-25 16:41:19 +01:00
Alex Auvolat d25e631a4a relocalize logic for write_sets 2025-03-25 16:35:56 +01:00
Alex Auvolat 514eb29874 use a WriteLock for write operations on fullcopy tables 2025-03-25 13:26:23 +01:00
Alex Auvolat 8ba6454e21 reduce anti-entropy interval for fullcopy tables 2025-03-25 13:11:11 +01:00
Alex Auvolat 9dcc5232a6 admin api: use fast local reads for token verification 2025-03-25 13:07:45 +01:00
Alex Auvolat 1e13a66b42 rework bucket helper functions to use local access where relevant 2025-03-25 13:00:48 +01:00
Alex Auvolat 2c9e849bbf remove dependency from garage_block to garage_table 2025-03-25 11:43:41 +01:00
Alex Auvolat 34baade499 fullcopy replication: quorum reads and writes 2025-03-25 11:40:09 +01:00
Alex Auvolat 2f2a96b51d layout & replication mode refactoring 2025-03-25 11:05:00 +01:00
Alex Auvolat c9156f6828 Merge branch 'main' into next-v2 2025-03-25 10:02:41 +01:00
Alex 14d2f2b18d Merge pull request 'update cargo dependencies' (#992) from update-deps into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/992
2025-03-21 09:06:06 +00:00
Alex Auvolat a7d845a999 change aws-sdk features to avoid using aws-lc which doesn't compile on i686/arm 2025-03-20 17:05:43 +01:00
Alex Auvolat dd20e5d22a update cargo dependencies 2025-03-20 13:36:01 +01:00
Alex 4629ee25f7 Merge pull request 'add crc64nvme checksumming algorithm (fix #963)' (#991) from add-crc64nvme into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/991
2025-03-20 12:28:35 +00:00
Alex Auvolat a826c361a9 add crc64nvme checksumming algorithm (fix #963) 2025-03-19 15:51:06 +01:00
Alex fb6db494cc Merge pull request 'sse-c: use different object encryption key for each object (fix #848)' (#990) from fix-ssec-crypto into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/990
2025-03-19 13:12:24 +00:00
Alex Auvolat 97e2fa5b8b add upgrade test for sse-c 2025-03-18 19:09:38 +01:00
Alex Auvolat cfd259190f sse-c: use different object encryption key for each object 2025-03-18 16:23:37 +01:00
maximilien 6906a4ff12 Merge pull request 'doc: add instructions on how to increase PVC size' (#987) from Joker9944/garage:main into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/987
Reviewed-by: maximilien <me@mricher.fr>
2025-03-17 20:32:31 +00:00
Joker9944 9053782d71 doc: add instructions on how to increase PVC size 2025-03-15 00:32:18 +01:00
Alex 48e0436f29 Merge pull request 'cli: uniformize output and add some infos' (#984) from uniformize-cli into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/984
2025-03-12 15:51:11 +00:00
Alex Auvolat 9c745548c4 test-upgrade with v1 -> v2 2025-03-12 16:06:28 +01:00
Alex Auvolat f7d9c2b383 cli: add garage json-api command and fix cargo tests 2025-03-12 15:55:52 +01:00
Alex Auvolat e6862c5d3d cli: uniformize output and add some infos 2025-03-12 15:05:37 +01:00
Alex d032e2017c Merge pull request 'admin api improvements' (#983) from admin-api-improvements into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/983
2025-03-12 11:00:24 +00:00
Alex Auvolat 0b12debf6c admin api: generate params from struct 2025-03-12 11:07:12 +01:00
Alex Auvolat 795b4a41b7 admin api: add special endpoints to openapi spec 2025-03-12 10:52:58 +01:00
Alex fd2472d488 Merge pull request 'support for multiple admin API token' (#982) from multi-admin-token into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/982
2025-03-12 09:30:19 +00:00
Alex Auvolat d2a064bb1b cli: add and remove scopes using --scope=+Scope or --scope=-Scope 2025-03-12 10:20:08 +01:00
Alex Auvolat 88b4623bf1 add creation date to admin api tokens 2025-03-12 09:57:32 +01:00
Alex Auvolat 325f79012c admin_token_table: implement is_tombstone() 2025-03-12 09:29:54 +01:00
Alex Auvolat eb40475f1e move bucket search logic from helper to admin api 2025-03-12 09:21:53 +01:00
Alex Auvolat 22c0420607 admin api: specify date-time format in openapi spec 2025-03-11 18:25:46 +01:00
Alex Auvolat 1bd7689301 cli: add functions to manage admin api tokens 2025-03-11 18:09:24 +01:00
Alex Auvolat ec0da3b644 admin api: mention admin_token and metrics_token in ListAdminTokensResponse 2025-03-11 15:57:29 +01:00
Alex Auvolat 9511b20153 admin api: add openapi spec for admin token management functions 2025-03-11 15:38:38 +01:00
Alex Auvolat d067a40b3f admin api: add functions to manage admin api tokens 2025-03-11 15:17:31 +01:00
Alex Auvolat ff6ec62d54 admin api: add metrics_require_token config option and update doc 2025-03-11 14:17:22 +01:00
Alex Auvolat 004eb94e14 admin api: verify tokens using the new admin api token table 2025-03-11 13:51:19 +01:00
Alex Auvolat 46f620119b add model for admin key table 2025-03-11 13:51:19 +01:00
Alex Auvolat 576d0d950e admin api: move functions to their correct location 2025-03-11 13:09:35 +01:00
Alex 85a07c87d7 Merge pull request 'admin refactoring, step 4' (#980) from refactor-admin into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/980
2025-03-11 09:19:12 +00:00
Alex Auvolat 1f645830a4 layout: make optional version mandatory in apply layout changes 2025-03-11 10:05:02 +01:00
Alex Auvolat 5f308bd688 move zone redundancy parsing/formatting to cli 2025-03-11 10:00:37 +01:00
Alex Auvolat df758e8e0d cli v2: simplify 2025-03-11 09:54:05 +01:00
Alex Auvolat e83864af24 layout: better encapsulation 2025-03-11 09:29:38 +01:00
Alex Auvolat 3b49dd9e63 admin api: small refactor + add comments to layout-related calls 2025-03-11 09:19:20 +01:00
Alex Auvolat cef8d75983 admin api: avoid overwriting redirect rules in UpdateBucket 2025-03-07 16:25:55 +01:00
Alex c96be1a9a8 Merge pull request 'doc/upgrading: slightly more precise wording' (#981) from Armael/garage:doc-upgrading into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/981
2025-03-07 15:16:24 +00:00
maximilien 98e56490a1 Merge pull request 'helm-chart: Fix headless service' (#976) from babykart/garage:headless-svc into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/976
2025-03-07 12:17:20 +00:00
Armaël Guéneau e791ccec8f doc/upgrading: slightly more precise wording 2025-03-07 12:27:21 +01:00
maximilien d605c4fed1 Explicitely set ClusterIP on headless service type
Signed-off-by: maximilien <maximilien@deuxfleurs.fr>
2025-03-07 09:17:05 +00:00
Alex Auvolat cd0728cd20 cli: move files around 2025-03-06 18:54:40 +01:00
Alex Auvolat 0951b5db75 admin api: implement ClusterLayoutSkipDeadNodes and use it in CLI 2025-03-06 18:49:56 +01:00
Alex Auvolat 3d94eb8d4b admin api: implement GetClusterLayoutHistory and use it in CLI 2025-03-06 18:33:05 +01:00
Alex Auvolat 004866caac admin api, cliv2: implement garage layout show using api functions 2025-03-06 17:56:22 +01:00
Alex Auvolat 913e6da41b admin api: implement PreviewClusterLayoutChanges 2025-03-06 17:27:23 +01:00
Alex Auvolat e4881e62f1 admin api: management of layout parameters through admin api 2025-03-06 17:12:52 +01:00
Alex 7ccbfda26d Merge pull request 'admin api: generate openapi spec using utoipa' (#979) from refactor-admin into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/979
2025-03-06 15:09:00 +00:00
Alex Auvolat 6b19d7628e admin api: small fixes and reordering 2025-03-06 14:21:25 +01:00
Alex Auvolat 411f1d495c admin api: add all missing endpoints to openapi spec 2025-03-06 14:01:39 +01:00
Alex Auvolat ba68506c36 admin api: generate openapi spec using utoipa (wip) 2025-03-06 12:26:14 +01:00
Alex 21c83ab311 Merge pull request 'admi api: remove info about local node from GetClusterStatus and add specific GetNodeInfo endpoint' (#978) from refactor-admin into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/978
2025-03-06 09:50:22 +00:00
Alex Auvolat 2e03d90585 admi api: remove info about local node from GetClusterStatus and add specific GetNodeInfo endpoint 2025-03-06 10:26:01 +01:00
babykart 0ce5f7eb00 helm-chart: Fix headless service
Signed-off-by: babykart <babykart@gmail.com>
2025-03-05 20:26:12 +01:00
Alex Auvolat 29ce490dd6 Merge branch 'main' into next-v2 2025-03-05 14:50:22 +01:00
Alex 516255321f Merge pull request 'doc: fix version number in quick start' (#974) from fix-quickstart into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/974
2025-03-05 11:07:27 +00:00
Alex Auvolat f3b05ff771 doc: fix version number in quick start 2025-03-05 12:06:05 +01:00
Alex e254cc20e5 Merge pull request 'Garage v1.1.0' (#968) from rel-1.1 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/968
2025-03-05 10:56:34 +00:00
Alex Auvolat 12f15c4c2b fix readme paths in cargo.toml for new crates 2025-03-05 11:00:19 +01:00
Alex Auvolat 42c5d02cdf doc: fix "since vX.X.X" in multiple places 2025-03-05 10:19:51 +01:00
Alex Auvolat 4689b10448 bump version to v1.1.0 2025-03-05 10:19:51 +01:00
Alex 156b10ee65 Merge pull request 'admin api definition: fix globalAlias query parameter name (related: #971)' (#973) from admin-sdk-fix into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/973
2025-03-05 09:19:30 +00:00
Alex Auvolat 8647ebf003 admin api definition: fix globalAlias query parameter name (related: #971) 2025-03-05 10:16:36 +01:00
maximilien 67d7c0769b Merge pull request 'Add headless service for statefulSet serviceName' (#970) from babykart/garage:helm-headless-svc into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/970
Reviewed-by: maximilien <me@mricher.fr>
2025-03-05 08:59:36 +00:00
babykart 09ed5ab8cc Fix documentation link
Signed-off-by: babykart <babykart@gmail.com>
2025-02-23 15:55:01 +01:00
babykart a0ea28b0da Add headless service for statefulSet serviceName
Signed-off-by: babykart <babykart@gmail.com>
2025-02-23 15:45:55 +01:00
Alex c5237c31e7 Merge pull request 'Implement all HTTP preconditions in GetObject/HeadObject' (#967) from fix-804 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/967
2025-02-19 17:31:26 +00:00
Alex Auvolat f87943a39d tests: add test for http preconditions 2025-02-19 18:26:03 +01:00
Alex Auvolat c0846c56fe api: unify http precondition handling 2025-02-19 18:14:27 +01:00
Alex 1cb0ae10a8 Merge pull request 'fix crash in layout computation when changing all nodes of a zone to gateway mode' (#937) from baptiste/garage:fix_layout_crash into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/937
Reviewed-by: Alex <lx@deuxfleurs.fr>
2025-02-19 17:09:10 +00:00
Alex Auvolat 1a8f74fc94 api: GetObject: implement if-match and if-unmodified-since 2025-02-19 17:26:29 +01:00
Alex 2191620af5 Merge pull request 'web: implement x-amz-website-redirect-location' (#966) from redirect-location-header into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/966
2025-02-19 16:10:04 +00:00
Alex Auvolat bf27a3ec98 web: implement x-amz-website-redirect-location 2025-02-19 17:04:10 +01:00
Alex f64ec6e542 Merge pull request 'implement STREAMING-*-PAYLOAD-TRAILER' (#960) from fix-824 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/960
2025-02-19 09:59:32 +00:00
Alex Auvolat 6d38907dac test: verify saved checksums in streaming putobject tests 2025-02-18 22:02:04 +01:00
Alex Auvolat cfe8e8d45c api: PutObject: save trailer checksum in metadata 2025-02-18 21:56:32 +01:00
Alex Auvolat f6e805e7db api: various fixes 2025-02-18 21:47:53 +01:00
Alex Auvolat 45e10e55f9 update aws-sdk-s3 in tests and fix wrong checksumming behavior in GetObject 2025-02-18 15:33:42 +01:00
Alex Auvolat 730bfee753 api: validate trailing checksum + add test for unsigned-paylad-trailer 2025-02-18 15:33:42 +01:00
Alex Auvolat ccab0e4ae5 api: fix optional \n after trailer checksum header 2025-02-18 15:33:42 +01:00
Alex Auvolat abb60dcf7e api: remove content-encoding: aws-chunked for streaming payload 2025-02-18 15:33:42 +01:00
Alex Auvolat f8b0817ddc api: streaming signature: fix trailer parsing 2025-02-18 12:00:41 +01:00
Alex Auvolat 21c0dda16a api: refactor: move checksumming code around again 2025-02-17 20:11:06 +01:00
Alex Auvolat 658541d812 api: use checksumming in api_common::signature for put/putpart 2025-02-17 19:54:25 +01:00
Alex Auvolat c5df820e2c api: start refactor of signature to calculate checksums earlier 2025-02-17 18:47:06 +01:00
Alex Auvolat a04d6cd5b8 api: streaming: parse unsigned streaming bodies and payload trailers 2025-02-17 16:23:24 +01:00
Alex Auvolat 44a896f9b5 api: add logic to parse x-amz-content-sha256 2025-02-16 18:25:35 +01:00
Alex Auvolat cee7560fc1 api: refactor: move checksum algorithms to common 2025-02-16 17:25:55 +01:00
Alex Auvolat 2f0c5ca220 signature: refactor: move constant defs to mod.rs 2025-02-16 16:34:18 +01:00
Alex 859b38b0d2 Merge pull request 'fix compilation warnings' (#959) from fixes into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/959
2025-02-14 17:32:30 +00:00
Alex Auvolat 2729a71d9d fix warning in garage test 2025-02-14 18:27:00 +01:00
Alex Auvolat c9d00f5f7b garage_api_s3: remove unused field in ListPartsQuery 2025-02-14 18:25:23 +01:00
Alex 89c944ebd6 Merge pull request 's3api: return Location in CompleteMultipartUpload (fix #852)' (#958) from fix-852 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/958
2025-02-14 17:16:58 +00:00
Alex Auvolat 24470377c9 garage_model: fix warning about dead code 2025-02-14 18:12:14 +01:00
Alex Auvolat 5b26545abf fix deprecated uses of chrono in lifecycle worker 2025-02-14 18:08:23 +01:00
Alex Auvolat 9c7e3c7bde remove cargo build options in makefile to avoid mistakes 2025-02-14 18:06:07 +01:00
Alex Auvolat 165f9316e2 s3api: return Location in CompleteMultipartUpload (fix #852)
NB. The location returned is not guaranteed to work in all cases.
This already fixes the parse issue in #852.
2025-02-14 18:05:07 +01:00
Alex a94adf804f Merge pull request 'block manager: avoid deadlock in fix_block_location (fix #845)' (#957) from fix-845 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/957
2025-02-14 16:53:01 +00:00
Alex Auvolat e4c9a8cd53 block manager: avoid deadlock in fix_block_location (fix #845) 2025-02-14 17:41:50 +01:00
Alex 9312c6bbcb Merge pull request 'Store data blocks only on nodes in the latest cluster layout version (fix #815)' (#956) from fix-815 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/956
2025-02-14 15:53:16 +00:00
Alex Auvolat fdf4dad728 block resync: avoid saving blocks to draining nodes 2025-02-14 16:45:55 +01:00
Alex Auvolat 6820b69f30 block manager: improve read strategy to find blocks faster 2025-02-14 16:45:55 +01:00
Alex Auvolat d0104b9f9b block manager: write blocks only to currently active layout version (fix #815)
avoid wastefully writing blocks to nodes that will discard them as soon
as the layout migration is finished
2025-02-14 16:45:55 +01:00
Alex 3fe8db9e52 Merge pull request 'web_server.rs: Added bucket domain to observability' (#608) from jpds/garage:domain-web-requests into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/608
2025-02-14 14:26:08 +00:00
Alex 627a37fe9f Merge pull request 's3 api: parse x-id query parameter and warn of any inconsistency (fix #822)' (#954) from fix-822 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/954
2025-02-14 14:07:01 +00:00
Alex Auvolat 2f55889835 add configuration option to enable/disable monitoring bucket in web metrics 2025-02-14 14:59:00 +01:00
Jonathan Davies 8b9cc5ca3f web_server.rs: Added bucket domain to observability. 2025-02-14 14:36:20 +01:00
Alex a1533d2919 Merge pull request 'cli: return info of all nodes when doing garage stats -a (fix #814)' (#953) from fix-814 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/953
2025-02-14 13:31:42 +00:00
Alex Auvolat c1b39d9ba1 s3 api: parse x-id query parameter and warn of any inconsistency (fix #822) 2025-02-14 14:30:58 +01:00
Alex Auvolat d84308c413 cli: return info of all nodes when doing garage stats -a (fix #814) 2025-02-14 14:11:41 +01:00
Alex c3e8e5e38c Merge pull request 'support redirection on s3 endpoint' (#910) from trinity-1686a/garage:1686a/s3-redirects into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/910
2025-02-14 12:58:48 +00:00
Alex Auvolat 62a3003cca rename Condition into RedirectCondition in internal model 2025-02-14 13:45:32 +01:00
Alex Auvolat 3151695011 Merge branch 'next-v2' into 1686a/s3-redirects 2025-02-14 13:11:33 +01:00
Alex 63f20bdeab Merge pull request 'db-snapshot: Add error handling to metadata snapshot creation' (#930) from handle_snapshot_errors into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/930
Reviewed-by: Armael <armael@noreply.localhost>
2025-02-14 11:52:58 +00:00
trinity-1686a f034e834fa Merge branch 'main' into 1686a/s3-redirects 2025-02-08 16:01:11 +01:00
trinity-1686a bf0f792418 add redirect_all to WebsiteConfig model 2025-02-08 15:36:48 +01:00
Baptiste Jonglez a2e134f036 db-snapshot: propagate any node snapshot error through RPC call
In particular, it means that "garage meta snapshot --all" will get an exit
code of 1 if any node fails to snapshot.

This makes sure that any external tool trying to snapshot nodes (e.g. from
cron) will be aware of the failure.

Fix #920
2025-02-07 00:29:43 +01:00
Baptiste Jonglez 06aa4b604f db-snapshot: Fix error reporting when using "garage meta snapshot --all"
Snapshot errors on remote nodes were not reported at all.

We now get proper error output such as:

    0fa0f35be69528ab  error: Internal error: DB error: LMDB: No space left on device (os error 28)
    88d92e2971d14bae  ok

Fix #920
2025-02-07 00:18:01 +01:00
Alex Auvolat 61f3de6496 Merge branch 'main' into next-v2 2025-02-05 21:00:19 +01:00
Alex 71655c1e89 Merge pull request 'Admin API refactoring: convert existing commands to API requests (step 3)' (#945) from refactor-admin into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/945
2025-02-05 19:54:42 +00:00
Alex d3226bfa91 Merge pull request 'remove uses of #[async_trait]' (#952) from remove-async-trait into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/952
2025-02-05 19:52:00 +00:00
Alex Auvolat af67626ab2 remove async_trait for TableRepair 2025-02-05 20:45:07 +01:00
Alex Auvolat 5475da8ea8 remove async_trait used in generic_server.rs 2025-02-05 20:31:34 +01:00
Alex Auvolat 620dc58560 remove async_trait for traits declared in garage_net 2025-02-05 20:22:16 +01:00
Alex Auvolat 7c8fc04b96 massively speed up compilation of garage_api_admin by not using async_trait 2025-02-05 19:37:38 +01:00
Alex Auvolat f914db057a cli_v2: implement LaunchRepairOperation and remove old stuff 2025-02-05 15:36:47 +01:00
Alex Auvolat 406b6da163 cli_v2: implement Get{Node,Cluster}Statistics 2025-02-05 15:06:10 +01:00
Alex Auvolat 9f468b4439 cli_v2: implement CreateMetadataSnapshot 2025-02-05 14:22:10 +01:00
Alex Auvolat 97be7b38fa admin api: reorder things 2025-02-03 18:54:51 +01:00
Alex Auvolat 6a1079c412 admin api: impl RequestHandler for MetricsRequest 2025-02-03 18:54:51 +01:00
Alex Auvolat b1629dd355 cli_v2: implement RetryBlockResync and PurgeBlocks 2025-02-03 18:54:51 +01:00
Alex Auvolat d405a9f839 cli_v2: implement ListBlockErrors and GetBlockInfo 2025-02-03 18:54:51 +01:00
Alex Auvolat 7b9c047b11 cli_v2: add local_api_request with crazy type bound 2025-02-03 18:54:51 +01:00
Alex Auvolat 10bbb26b30 cli_v2: implement ListWorkers and GetWorkerInfo 2025-02-03 18:54:51 +01:00
Alex Auvolat 89ff9f5576 admin api: base infrastructure for local endpoints
admin api: rename EndpointHandler into RequestHandler to avoid confusion with RPC

wip: infrastructure for local api calls

admin api: fix things

admin api: first local endpoint to work with new scheme

admin api: implement SetWorkerVariable
2025-02-03 18:54:51 +01:00
Alex Auvolat bdaf55ab3f cli_v2: migrate cleanupincompleteuploads to Admin API
admin api: add CleanupIncompleteUploads spec
2025-02-03 18:54:51 +01:00
Alex Auvolat e96014ca60 Merge branch 'main' into next-v2 2025-02-03 18:53:17 +01:00
Alex 47e87c8739 Merge pull request 'upgrade Rust compiler and Cargo dependencies' (#951) from nix-crane into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/951
2025-02-03 17:49:00 +00:00
Alex Auvolat 34599bff51 update all Cargo dependencies except AWS crates and their dependencies 2025-02-03 17:46:54 +01:00
Alex Auvolat ec1a475923 build with rust 1.82.0 2025-02-03 17:46:48 +01:00
Alex b9df2d1ad1 Merge pull request 'compile with crane' (#950) from nix-crane into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/950
2025-02-03 15:54:54 +00:00
Alex Auvolat 390a5d97fe nix, ci: build with Crane
This removes our dependency on cargo2nix, which was causing us some
issues. Whereas cargo2nix creates one Nix derivation per crate, Crane
uses only two derivations:

1. Build dependencies only
2. Build the final binary

This means that during the second step, no caching can be done. For
instance, if we do a change in garage_model, we need to recompile all of
the Garage crates including those that do not depend on garage_model.
On the upside, this allows all of the Garage crates to be built at once
using cargo build logic, which is optimized for high parallelism and
better pipelining between all of the steps of the build. All in all,
this makes most builds faster than cargo2nix.

A few other changes have been made to the build scripts and CI:

- Unit tests are now run within a Nix derivation. In fact, we have
  different derivations to run the tests using LMDB and Sqlite as
  metadata db engines.

- For debug builds, most CI steps now run in parallel (with the notable
  exception of the smoke test that runs after the build, which is
  inevitable).

- We no longer pass the GIT_VERSION argument when building debug builds
  and running the tests. This means that dev binaries and test
  binaries don't know the exact version of Garage they are from. That
  shouldn't be an issue in most cases.

- The not-dynamic.sh scripts has been fixed to fail if the file does not
  exist.
2025-02-03 16:39:50 +01:00
Alex Auvolat 568c4954e9 Merge branch 'main' into next-v2 2025-02-01 19:58:36 +01:00
Alex 4dc2bc337f Merge pull request 'woodpecker: use parallel nix-build in debug builds' (#949) from nix-parallel into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/949
2025-02-01 18:58:15 +00:00
Alex Auvolat 5dd2791981 woodpecker: use parallel nix-build in debug builds 2025-02-01 19:48:01 +01:00
Alex Auvolat fe937c2901 Merge branch 'main' into next-v2 2025-02-01 19:07:17 +01:00
Alex d601f31186 Merge pull request 'split garage_api in garage_api_{common,s3,k2v,admin}' (#947) from split-garage-api into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/947
2025-02-01 17:48:25 +00:00
Alex Auvolat e4de7bdfd5 fix ci for more test crates 2025-01-31 19:21:36 +01:00
Alex Auvolat d18c5ad0ff fix tests 2025-01-31 19:12:51 +01:00
Alex Auvolat 3d5e9a027e cargo defs: simplify and fix descriptions 2025-01-31 18:54:29 +01:00
Alex Auvolat f4ca7758b4 update cargo.nix 2025-01-31 18:48:07 +01:00
Alex Auvolat 4563313f87 use cargo-shear to remove many unused dependencies between crates 2025-01-31 18:47:30 +01:00
Alex Auvolat afa28706e5 split s3/cors.rs into also common/cors.rs 2025-01-31 18:42:14 +01:00
Alex Auvolat 84f1db91c4 fix things up 2025-01-31 18:34:57 +01:00
Alex Auvolat 9fa20d45be wip: split garage_api into garage_api_{common,s3,k2v,admin} 2025-01-31 18:18:29 +01:00
Alex 9330fd79d3 Merge pull request 'table::insert_many: avoid failure with zero items (fix #915)' (#946) from fix-915 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/946
2025-01-31 13:10:54 +00:00
Alex Auvolat 83f6928ff7 table::insert_many: avoid failure with zero items (fix #915) 2025-01-30 18:06:47 +01:00
Alex 3192088aac Merge pull request 'admin refactoring: refactor CLI to use Admin API requests (step 2)' (#943) from refactor-admin into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/943
2025-01-30 16:18:38 +00:00
Alex Auvolat 5a89350b38 cli_v2: fix garage status 2025-01-30 16:45:59 +01:00
Alex Auvolat 3caea5fc06 cli_v2: merge util.rs into layout.rs 2025-01-30 16:24:55 +01:00
Alex Auvolat ebc0e9319e cli_v2: error messages 2025-01-30 16:17:35 +01:00
Alex Auvolat f8c6a8373d convert cli key operations to admin rpc 2025-01-30 16:12:16 +01:00
Alex Auvolat 076ce04fe5 fix garage status output 2025-01-30 15:38:22 +01:00
Alex Auvolat f37d5d2b08 admin api: convert most bucket operations 2025-01-30 13:36:25 +01:00
Alex Auvolat 819f4f0050 cli: migrate layout remove, apply, revert 2025-01-30 12:19:23 +01:00
Alex Auvolat 69ddaafc60 wip: migrate garage status and garage layout assign 2025-01-30 12:07:12 +01:00
Alex Auvolat 145130481e wip: proxy admin api requests through admin rpc, prepare new cli 2025-01-30 10:44:08 +01:00
Alex 6ed78abb5c Merge pull request 'admin API refactoring (step 1)' (#939) from refactor-admin into next-v2
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/939
2025-01-29 20:42:56 +00:00
Alex Auvolat 19454c1679 admin api: remove log message 2025-01-29 19:47:37 +01:00
Alex Auvolat 1c03941b19 admin api: fix panic on GetKeyInfo with no args 2025-01-29 19:26:16 +01:00
Alex Auvolat 4f0b923c4f admin api: small fixes 2025-01-29 19:26:16 +01:00
Alex Auvolat 420bbc162d admin api: clearer syntax for AddBucketAlias and RemoveBucketAlias 2025-01-29 19:26:16 +01:00
Alex Auvolat 12ea4cda5f admin api: merge calls to manage global/local aliases 2025-01-29 19:26:16 +01:00
Alex Auvolat 5fefbd94e9 admin api: rename allow/deny api calls in api v2 2025-01-29 19:26:16 +01:00
Alex Auvolat ba810b2e81 admin api: rename bucket aliasing operations 2025-01-29 19:26:16 +01:00
Alex Auvolat f8ed3fdbc4 fix test_website_check_domain 2025-01-29 19:26:16 +01:00
Alex Auvolat 2daeb89834 admin api: fixes to openapi v2 spec 2025-01-29 19:26:16 +01:00
Alex Auvolat 4cb45bd398 admin api: fix CORS to work in browser 2025-01-29 19:26:16 +01:00
Alex Auvolat d5ad797ad7 admin api: update v2 openapi spec 2025-01-29 19:26:16 +01:00
Alex Auvolat a99925e0ed admin api: initialize v2 openapi spec from v1 2025-01-29 19:26:16 +01:00
Alex Auvolat f538dc34d3 admin api: make all requests and responses (de)serializable 2025-01-29 19:26:16 +01:00
Alex Auvolat ed58f8b0fe admin api: update semantics of some endpoints, and update doc 2025-01-29 19:26:16 +01:00
Alex Auvolat 5037b97dd4 admin api: add compatibility from v1/ to v2/ 2025-01-29 19:26:16 +01:00
Alex Auvolat af1a530834 admin api: refactor using macro 2025-01-29 19:26:16 +01:00
Alex Auvolat c99bfe69ea admin api: new router_v2 with unified path syntax 2025-01-29 19:26:16 +01:00
Alex Auvolat 831f2b0207 admin api: make all handlers impls of a single trait 2025-01-29 19:26:16 +01:00
Alex Auvolat c1eb1610ba admin api: create structs for all requests/responess in src/api/admin/api.rs 2025-01-29 19:26:16 +01:00
Quentin Dufour 5560a963e0 decrease write quorum 2025-01-29 19:25:58 +01:00
Alex ab71544499 Merge pull request 'api: better handling of helper errors to distinguish error codes' (#942) from fix-getkeyinfo-404 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/942
2025-01-29 18:25:44 +00:00
Alex 991edbe02c Merge pull request 'Update doc/book/connect/repositories.md' (#941) from yatesco/garage:main into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/941
2025-01-29 18:18:59 +00:00
Alex Auvolat 9f3c7c3720 api: better handling of helper errors to distinguish error codes 2025-01-29 19:14:34 +01:00
yatesco bfde9152b8 Update doc/book/operations/multi-hdd.md
trivial spelling mistake
2025-01-29 13:40:41 +00:00
yatesco 7bb042f0b7 Update doc/book/connect/repositories.md
trivial spelling mistake
2025-01-29 13:34:35 +00:00
Alex a1d081ee84 Merge pull request 's3 api: make x-amz-meta-* headers lowercase (fix #844)' (#938) from fix-844 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/938
2025-01-27 19:32:19 +00:00
Alex Auvolat e8fa89e834 s3 api: make x-amz-meta-* headers lowercase (fix #844) 2025-01-27 19:58:06 +01:00
Alex beedc9fd11 Merge pull request 'snapshot: sqlite: use a subdirectory for consistency with LMDB' (#932) from baptiste/garage:snapshot_consistency_sqlite into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/932
2025-01-27 18:50:11 +00:00
Baptiste Jonglez 6d798c640f WIP: fix crash in layout computation when changing all nodes of a zone to gateway mode
This change is probably not a proper fix, somebody with more expertise on
this code should look at it.

Here is how to reproduce the crash:

- start with a layout with two zones
- move all nodes of a zone to gateway mode: `garage layout assign fea54bcc081f318 -g`
- `garage layout show` will panic with a backtrace

Fortunately, the crash is only on the RPC client side, not on the Garage
server itself, and `garage layout revert` still works to go back to the
previous state.

As far as I can tell, this bug is present since Garage 0.9.0 which
includes the new layout assignation algorithm:

  https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/296
2025-01-27 19:33:57 +01:00
Alex d4e3e60920 Merge pull request 'update nix crate to 0.29 and libc to 0.2.169' (#931) from neuschaefer/garage:nix into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/931
2025-01-27 18:09:51 +00:00
Baptiste Jonglez 43402c9619 snapshot: sqlite: use a subdirectory for consistency with LMDB
Currently, taking a snapshot of the metadata database with sqlite creates
a sqlite file without extension with the following format:

    snapshots/2025-01-26T15:29:17Z

This makes it hard to understand what kind of data this is, and is not
consistent with LMDB:

    snapshots/2025-01-26T15:29:17Z/data.mdb

With this change, we now get a directory with a single db.sqlite file:

    snapshots/2025-01-26T15:29:17Z/db.sqlite
2025-01-27 19:06:52 +01:00
Alex efa6f3d85e Merge pull request 'db-snapshot: allow to set directory where snapshots are stored' (#933) from baptiste/garage:configure_metadata_snapshots_dir into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/933
2025-01-27 18:04:05 +00:00
Alex Auvolat 74a1b49b13 Update Cargo.nix 2025-01-27 18:37:05 +01:00
J. Neuschäfer 23d57b89dc update nix crate to 0.29 and libc to 0.2.169 2025-01-27 18:37:05 +01:00
Alex 5e3e1f4453 Merge pull request 'fix problems with CI doing work multiple times' (#936) from woodpecker-simplify into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/936
2025-01-27 17:36:27 +00:00
Baptiste Jonglez 59c153d280 db-snapshot: allow to set directory where snapshots are stored
Fix #926
2025-01-27 18:33:55 +01:00
Alex Auvolat bb3e0f7d22 nix CI: reduce redundant work 2025-01-27 18:09:51 +01:00
Alex 0156e40c9d Merge pull request 'ci: fix woodpecker definitions to comply with woodpecker 3' (#935) from woodpecker3 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/935
Reviewed-by: maximilien <me@mricher.fr>
2025-01-27 12:03:46 +00:00
Alex Auvolat f6f88065ad ci: fix woodpecker definitions to comply with woodpecker 3 2025-01-27 12:06:31 +01:00
Alex 591bd808ec Merge pull request 'doc: Fix Nix devenv setup' (#927) from fix_devenv into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/927
2025-01-23 10:20:04 +00:00
maximilien 294cb99409 Merge pull request 'Fix all typos' (#928) from majst01/garage:fix-typos into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/928
Reviewed-by: maximilien <me@mricher.fr>
2025-01-17 06:06:14 +00:00
Stefan Majer 2eb9fcae20 Fix all typos 2025-01-16 13:22:00 +01:00
Baptiste Jonglez 58b9eb46fc doc: Fix Nix devenv setup
This is a hotfix to fix the doc for the current setup, see #868 for
possible future directions.
2025-01-16 10:00:12 +01:00
maximilien 255b01b626 Merge pull request 'Helm chart: Add garage.existingConfigmap and replace garage.garage.toml with garage.garageTomlString' (#923) from jessebot/garage:allow-existing-configmap into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/923
Reviewed-by: maximilien <me@mricher.fr>
2025-01-15 23:53:25 +00:00
Maximilien R. 58a765c51f Minor rewording, add some more hints 2025-01-15 23:51:07 +00:00
jessebot 1c431b8457 Add garage.existingConfigmap and replace garage.garage.toml with garage.garageTomlString
also moves all gotemplating back to configmap

and adds autogenerated docs via helm-docs

Signed-off-by: jessebot <jessebot@linux.com>
2025-01-15 23:51:07 +00:00
Alex 39ac034de5 Merge pull request 'update toolchain' (#924) from nix-update into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/924
2025-01-13 10:19:53 +00:00
Alex Auvolat 8ddb0dd485 nix build: switch to upstream cargo2nix (branch release-0.11.0) 2025-01-12 18:16:23 +01:00
Alex Auvolat 83887a8519 nix build: remove clippy build env that doesn't work 2025-01-12 17:51:33 +01:00
Alex Auvolat 0a15db6960 nix build: update rustc to v1.78 2025-01-12 17:37:36 +01:00
Alex Auvolat 295237476e fix formatting to comply with latest rustfmt 2025-01-12 17:36:25 +01:00
Alex Auvolat 9d83605736 flake: update versions of nixpkgs and rust-overlay 2025-01-12 17:34:04 +01:00
maximilien 4b1a7fb5e3 Merge pull request 'The version flag is required when applying a layout' (#921) from update-quickstart-docs-layout-apply into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/921
2025-01-09 00:41:35 +00:00
fabientot b6aaebaf4c The version flag is required when applying a layout
I followed the documentation and got an error if the layout's version was not specified 

```
garage layout apply

Error: Internal error:
Please pass the new layout version number to ensure that you are writing the correct version of the cluster layout.
To know the correct value of the new layout version, invoke `garage layout show` and review the proposed changes.
```

This fixes that
2025-01-08 20:30:09 +00:00
Alex Auvolat 2aaba39ddc refactor web_server.rs 2025-01-04 20:11:54 +01:00
Alex Auvolat 47467df83e avoid handling status_code-related logic in api/s3/get.rs 2025-01-04 19:52:14 +01:00
Alex Auvolat 9b7fea4cb0 put bucket website: improve error message for redirectallrequests 2025-01-04 19:16:24 +01:00
Alex Auvolat 44ce6ae5b4 properly implement new bucket model using a migration 2025-01-04 18:50:49 +01:00
Alex Auvolat 22487ceddf move Redirect::compute_target to standalone function in web_server.rs 2025-01-04 18:22:42 +01:00
Alex 7bbc8fec50 Merge pull request 'Fix #907' (#917) from vk/garage:fix_907 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/917
2025-01-04 16:07:40 +00:00
Alex Auvolat 6ccfbb2986 remove obsolete RedirectAll struct 2025-01-04 17:04:17 +01:00
Vedad KAJTAZ 6689800986 Formatting with 2025-01-04 16:52:23 +01:00
Alex d2246baab7 Merge pull request 'update flake.lock' (#918) from update-flake into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/918
2025-01-04 15:43:41 +00:00
Alex Auvolat afac1d4d4a update flake.lock 2025-01-04 16:29:42 +01:00
Vedad KAJTAZ 6ca99fd02c formatting 2025-01-04 14:46:42 +01:00
Vedad KAJTAZ b568bb863d Fix #907 2025-01-04 12:50:10 +01:00
Alex b8f301a61d Merge pull request 'woodpecker: use modern syntax for secrets (removes warning)' (#912) from woodpecker-fix-warnings into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/912
2024-12-23 17:41:15 +00:00
Alex Auvolat 428ad2075d woodpecker: use modern syntax for secrets (removes warning) 2024-12-23 18:00:22 +01:00
trinity-1686a c939d2a936 clippy 2024-12-22 15:26:06 +01:00
trinity-1686a 65e9dde8c9 add tests 2024-12-22 15:20:09 +01:00
trinity-1686a c9b733a4a6 support redirection on s3 endpoint 2024-12-14 17:46:27 +01:00
maximilien 3661a597fa Merge pull request 'feat: add use_local_tz configuration' (#908) from ragazenta/garage:feat/local-timezone into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/908
Reviewed-by: maximilien <me@mricher.fr>
2024-12-01 13:23:24 +00:00
Renjaya Raga Zenta 0fd3c0e794 doc: add use_local_tz configuration 2024-11-25 10:35:00 +07:00
Renjaya Raga Zenta 4c1bf42192 feat: add use_local_tz configuration
Used in lifecycle_worker to determine midnight time
2024-11-23 05:51:12 +07:00
maximilien 906c8708fd Merge pull request 'add extraVolumes and extraVolumeMounts to helm chart' (#896) from eugene-davis/garage:main into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/896
Reviewed-by: maximilien <me@mricher.fr>
2024-11-19 22:23:13 +00:00
Alex 747889a096 Merge pull request 'Update Python SDK documentation' (#887) from cryptolukas/garage:fix-python-sdk-docs into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/887
2024-11-19 09:15:03 +00:00
Alex feb09a4bc6 Merge pull request 'doc: update mastodon media header pruning section' (#888) from teutat3s/garage:doc-update-mastodon-media into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/888
2024-11-19 09:14:34 +00:00
maximilien aa8bc6aa88 Merge pull request 'doc: add Triplebit's use-case' (#901) from jonah/garage:triplebit into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/901
Reviewed-by: maximilien <me@mricher.fr>
2024-11-17 13:43:49 +00:00
Jonah Aragon aba7902995 doc: add Triplebit's use-case 2024-11-15 16:27:46 -06:00
Alex 78de7b5bde Merge pull request 'fix bit/byte inversion in rpc secret error message' (#898) from trinity-1686a/garage:rpc-comment into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/898
2024-11-07 11:11:12 +00:00
trinity-1686a 9bd9e392ba fix bit/byte inversion in rpc secret error message 2024-11-07 00:29:26 +01:00
Eugene Davis 116ad479a8 add extraVolumes and extraVolumeMounts to helm chart 2024-10-26 21:14:08 +02:00
teutat3s b6a58c5c16 doc: update mastodon media header pruning section
This is now possible since the upstream issue has been resolved.
https://github.com/mastodon/mastodon/issues/9567
2024-10-17 20:59:21 +02:00
Matthias Doering 2b0bfa9b18 the old value do not work out of the box 2024-10-14 17:20:26 +02:00
Alex a18b3f0d1f Merge pull request 'Garage v1.0.1' (#881) from rel-v1.0.1 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/881
2024-09-22 13:02:02 +00:00
Alex Auvolat 7a143f46fc Bump to version 1.0.1 2024-09-22 14:25:32 +02:00
Alex c731f0291a Merge pull request 'fix logic in garage layout skip-dead-nodes + fix typo (fix #879)' (#880) from fix-skip-dead-nodes into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/880
2024-09-22 12:01:49 +00:00
Alex Auvolat 34453bc9c2 fix logic in garage layout skip-dead-nodes + fix typo (fix #879) 2024-09-22 13:47:27 +02:00
Alex 6da1353541 Merge pull request 'Don't fetch old values in cross-partition transactional inserts' (#877) from withings/garage:perf/kv/insert-no-return-cross-partition into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/877
2024-09-14 15:57:27 +00:00
Julien Kritter bd71728874 Tests: don't expect old value after transactional insert 2024-09-12 10:50:53 +02:00
Julien Kritter 51ced60366 Don't fetch old values in cross-partition transactional inserts 2024-09-12 10:26:28 +02:00
Alex 586957b4b7 Merge pull request 'KV: don't retrieve values for write ops' (#873) from marvinj97/garage:perf/kv/insert-no-return into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/873
Reviewed-by: Alex <alex@adnab.me>
2024-09-10 09:06:29 +00:00
Alex 8d2bb4afeb Merge pull request 'Typo' (#875) from faust/garage:doc2 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/875
2024-09-10 09:03:31 +00:00
Faustin Lammler c26f32b769 Typo
And remove trailing white space.
2024-09-10 09:34:59 +02:00
marvin-j97 8062ec7b4b test: fix db tests 2024-09-04 19:24:36 +02:00
marvin-j97 eb416a02fb dont assert deletion count in sqlite KV adapter 2024-09-04 18:51:51 +02:00
marvin-j97 74363c9060 perf(kv): dont retrieve values for write ops
see https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/851
2024-09-04 18:45:17 +02:00
Alex 615698df7d Merge pull request 'update compiler to Rust 1.77' (#866) from rust-1.77 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/866
2024-08-26 19:08:00 +00:00
Alex Auvolat 7061fa5a56 use rust 1.77 in nix/compile.nix 2024-08-26 19:19:16 +02:00
Alex Auvolat 8881930cdd update nixpkgs and rust-overlay sources in flake.nix 2024-08-26 19:19:16 +02:00
Alex 52f6c0760b Merge pull request 'update crate time (fix #849)' (#865) from update-time into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/865
2024-08-26 16:20:04 +00:00
Alex Auvolat 5b0602c7e9 update crate time (fix #849) 2024-08-26 18:11:21 +02:00
Alex 182b2af7e5 Merge pull request 'api servers: kill opened connections after SIGINT after 10s deadline (fix #806)' (#864) from exit-deadline into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/864
2024-08-25 18:34:55 +00:00
Alex Auvolat baf32c9575 api servers: kill opened connections after SIGINT after 10s deadline (fix #806) 2024-08-25 20:04:56 +02:00
Alex 3dda1ee4f6 Merge pull request 'fix build when lmdb feature is disabled (fix #800)' (#863) from fix-800 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/863
2024-08-25 10:00:40 +00:00
Alex Auvolat aa7ce9e97c fix build when lmdb feature is disabled (fix #800) 2024-08-25 11:42:37 +02:00
Alex 8d62616ec0 Merge pull request 'layout: discard old info when it is completely out-of-date (fix #841)' (#861) from fix-841 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/861
2024-08-24 11:12:39 +00:00
Alex bd6fe72c06 Merge pull request 'Quick start: mention Docker (replace #803)' (#862) from dougreeder into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/862
2024-08-24 11:07:46 +00:00
Alex Auvolat 4c9e8ef625 doc: clarify quick start on using docker 2024-08-24 13:07:02 +02:00
Alex 3e711bc110 Merge pull request 'don't modify postobject request before validating policy' (#850) from trinity-1686a/garage:fix-acl-postobject into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/850
2024-08-24 10:49:14 +00:00
Alex Auvolat 7fb66b4944 layout: discard old info when it is completely out-of-date (fix #841) 2024-08-24 12:38:56 +02:00
Quentin 679ae8bcbb Merge pull request 'Set "no read ahead" on LMDB to improve performances when "LMDB size" > "RAM size"' (#855) from fix-lmdb-no-read-ahead into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/855
Reviewed-by: Alex <alex@adnab.me>
2024-08-18 12:25:35 +00:00
Quentin Dufour 2a93ad0c84 force flag "no read ahead" on LMDB 2024-08-17 21:17:15 +02:00
trinity-1686a f190032589 don't modify postobject request before validating policy 2024-08-10 20:10:47 +02:00
maximilien 3a87bd1370 Merge pull request 'Improve error message for malformed RPC secret key' (#846) from improve-secret-error-message into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/846
Reviewed-by: Quentin <quentin@dufour.io>
2024-08-09 06:47:11 +00:00
Maximilien R. 9302cd42f0 Improve error message for malformed RPC secret key 2024-08-08 23:05:24 +00:00
kosssi 060ad0da32 docs: Update LMDB website 2024-08-06 21:47:14 +00:00
maximilien a5ed1161c6 Merge pull request 'Add environment variable dict to helm chart.' (#843) from Benjamin/garage:main into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/843
Reviewed-by: maximilien <me@mricher.fr>
2024-08-06 21:45:35 +00:00
Benjamin von Mossner 222674432b This commit adds an environment dict to garage helm chart. Using it, env variables can be set into the garage container environment, useful to set eg. GARAGE_ADMIN_TOKEN or GARAGE_METRICS_TOKEN 2024-07-25 11:42:13 +02:00
Alex 070a8ad110 Merge pull request 'doc: fix typo' (#831) from Armael/garage:typo into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/831
2024-06-18 12:40:32 +00:00
Alex 770384cae1 Merge pull request 'add rpc_public_addr_subnet config option' (#817) from flokli/garage:rpc_public_addr_subnet into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/817
Reviewed-by: Alex <alex@adnab.me>
2024-06-18 12:40:07 +00:00
Florian Klink a0f6bc5b7f add rpc_public_addr_subnet config option
In case `rpc_public_addr` is not set, but autodiscovery is used, this
allows filtering the list of automatically discovered IPs to a specific
subnet.

For example, if nodes should pick *their* IP inside a specific subnet,
but you don't want to explicitly write the IP down (as it's dynamic, or
you want to share configs across nodes), you can use this option.
2024-06-05 08:41:36 +02:00
Armaël Guéneau 88c734bbd9 typo 2024-06-04 15:34:02 +02:00
maximilien d38509ef4b Merge pull request 'adding the ability to change the default podManagementPolicy for StatefulSets' (#823) from bodaciousbiscuits/garage:main into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/823
Reviewed-by: maximilien <me@mricher.fr>
2024-05-25 18:35:53 +00:00
bodaciousbiscuits 39b37833c5 adding the ability to change the default podManagementPolicy for StatefulSets 2024-05-19 21:31:19 -05:00
maximilien a2c1de646b Merge pull request 'cli: clarify garage block is node-local' (#813) from flokli/garage:block-node-local into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/813
2024-05-12 08:53:26 +00:00
Florian Klink 15847a636a cli: clarify garage block is node-local
Prevents some of the confusion from
https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/810.
2024-05-07 07:42:33 +00:00
Alex 123d3e1f04 Merge pull request 'flake.nix: add rust-analyzer to devShells.full, expose devShells.full in shell.nix' (#816) from flokli/garage:shell-fixes into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/816
2024-04-23 18:54:26 +00:00
Florian Klink a6e4b96ca9 shell.nix: expose devShellFull
This allows accessing devShells.full from shell.nix.
2024-04-23 11:59:37 +03:00
Florian Klink b442b0e35e devShells.full: add rust-analyzer 2024-04-23 11:59:37 +03:00
P. Douglas Reeder 0c3b198b22 Improves Quick Start for users not using Linux 2024-04-10 16:42:10 -04:00
Alex 33c2086d9e Merge pull request '[fix-doc] fix broken references in documentation' (#802) from fix-doc into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/802
2024-04-10 15:49:03 +00:00
Alex Auvolat 5ad1e55ccf [fix-doc] fix broken references in documentation 2024-04-10 17:47:34 +02:00
Alex 1779fd40c0 Merge pull request 'Garage v1.0' (#683) from next-0.10 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/683
2024-04-10 15:23:12 +00:00
Alex Auvolat ff093ddbb8 Merge branch 'main' into next-0.10 2024-04-10 14:38:14 +02:00
Alex Auvolat 90e3c2af91 [next-0.10] small updates to mention Garage v0.9.4 2024-04-10 14:35:30 +02:00
Alex b47706809c Merge pull request 'fix typo in doc' (#799) from fix-typo into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/799
2024-04-08 15:09:27 +00:00
Alex Auvolat 126e0f47a3 fix typo in doc 2024-04-08 17:08:44 +02:00
Alex 738bb2f09c Merge pull request 'Garage v0.9.4' (#798) from rel-0.9.4 into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/798
2024-04-08 09:57:03 +00:00
Alex Auvolat 50669b3e76 [next-0.10] bump helm chart version 2024-04-03 14:19:59 +02:00
Alex Auvolat 554437254e [next-0.10] Add migration guide for v1.0 2024-03-28 18:45:06 +01:00
Alex Auvolat afad62939e [next-0.10] bump version number to 1.0 2024-03-28 15:19:44 +01:00
Alex Auvolat 8bfc16ba7d Merge branch 'main' into next-0.10 2024-03-28 15:01:05 +01:00
Alex Auvolat 25c196f34d [next-0.10] admin api: fix logic in get cluster status 2024-03-27 13:55:49 +01:00
Alex Auvolat 4eba32f29f [next-0.10] layout helper: rename & clarify updates to update trackers 2024-03-27 13:47:06 +01:00
Alex Auvolat 32f1786f9f [next-0.10] cache layout check result 2024-03-27 13:37:20 +01:00
Alex Auvolat 01a0bd5410 [next-0.10] remove impl Deref for LayoutHelper 2024-03-27 13:32:13 +01:00
Alex Auvolat c0eeb0b0f3 [next-0.10] fixes to k2v rpc + comment fixes 2024-03-27 10:44:03 +01:00
Alex Auvolat 51d11b4b26 [next-0.10] doc: 2 changes
- rewrite section on encryption to mention SSE-C
- change to real-world to make it closer to main branch
2024-03-27 10:10:45 +01:00
Alex Auvolat f7cd4eb600 Merge branch 'main' into next-0.10 2024-03-26 16:34:40 +01:00
Alex cdde0f19ee Merge pull request 'checksum algorithms' (#787) from s3-checksum into next-0.10
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/787
2024-03-26 14:24:58 +00:00
Alex Auvolat 74949c69cb [s3-checksum] implement x-amz-checksum-* headers 2024-03-26 15:01:34 +01:00
Alex 7e0107c47d Merge pull request 'Fixes to garage_net peering manager' (#786) from net-fixes into next-0.10
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/786
2024-03-21 10:26:36 +00:00
Alex Auvolat 3844110cd0 [net-fixes] netapp peer exchange: send only currently connected address 2024-03-21 10:50:44 +01:00
Alex Auvolat 961b4f9af3 [net-fixes] fix issues with local peer address (fix #761) 2024-03-21 10:45:34 +01:00
Alex Auvolat 5225a81dee [net-fixes] peering: only count node IDs and not addresses in hash 2024-03-21 09:47:04 +01:00
Alex Auvolat ce69dc302c Merge branch 'main' into next-0.10 2024-03-19 17:17:46 +01:00
Alex 65853a4863 Merge pull request 'block refcount repair' (#782) from block-ref-repair into next-0.10
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/782
2024-03-19 15:59:19 +00:00
Alex Auvolat 3eab639c14 [block-ref-repair] mention garage block repair-rc in documentation 2024-03-19 16:24:34 +01:00
Alex Auvolat 3165ab926c [block-ref-repair] rename rc's rc field to rc_table 2024-03-19 16:20:22 +01:00
Alex Auvolat dc0b78cdb8 [block-ref-repair] Block refcount recalculation and repair
- We always recalculate the reference count of a block before deleting
  it locally, to make sure that it is indeed zero.

- If we had to fetch a remote block but we were not able to get it,
  check that refcount is indeed > 0.

- Repair procedure that checks everything
2024-03-19 16:20:22 +01:00
Alex Auvolat 0038ca8a78 Merge branch 'main' into next-0.10 2024-03-18 20:19:30 +01:00
Alex 81191d2d92 Merge pull request 'Remove Sled' (#767) from rm-sled into next-0.10
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/767
2024-03-12 10:45:57 +00:00
Alex Auvolat 2795b53b8b [rm-sled] factorize some code in sqlite backend 2024-03-12 11:15:26 +01:00
Alex Auvolat 32aa246300 [rm-sled] Make proper use of pinning in LMDB adapter + comment unsafe 2024-03-08 17:39:17 +01:00
Alex Auvolat b942949940 [rm-sled] Implement iterators in sqlite & lmdb transactions
with way too much unsafe code
2024-03-08 16:38:01 +01:00
Alex Auvolat 66c23890c1 [rm-sled] Implement some missing functionality in garage_db 2024-03-08 16:02:58 +01:00
Alex Auvolat 05c92204ec [rm-sled] Remove counted_tree_hack 2024-03-08 15:09:57 +01:00
Alex 2128b5febd Merge pull request 'Remove migration path from Garage v0.5' (#766) from rm-migration into next-0.10
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/766
2024-03-08 13:43:42 +00:00
Alex Auvolat 44454aac01 [rm-sled] Remove the Sled database engine 2024-03-08 14:11:02 +01:00
Alex Auvolat 1ace34adbb Merge branch 'main' into next-0.10 2024-03-08 13:57:10 +01:00
Alex Auvolat f537f76681 [rm-migration] Remove migration path from Garage v0.5 2024-03-08 13:24:47 +01:00
Alex 20c0b4ffb2 Merge pull request 'ReplicationMode -> ConsistencyMode+ReplicationFactor' (#750) from yuka/garage:split-consistency-mode into next-0.10
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/750
2024-03-07 16:32:52 +00:00
Alex 2fd13c7d13 Merge pull request 'SSE-C encryption' (#730) from sse-c into next-0.10
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/730
2024-03-07 15:21:37 +00:00
Alex Auvolat 3fcb54e3cf [sse-c] Remove special case for Content-Type header 2024-03-07 15:43:48 +01:00
Alex Auvolat e3333f2ac5 [sse-c] Documentation for SSE-C 2024-03-07 15:43:48 +01:00
Alex Auvolat fa4878bad6 [sse-c] Testing for SSE-C encryption 2024-03-07 15:43:48 +01:00
Alex Auvolat 57acc60082 [sse-c] Implement SSE-C encryption 2024-03-07 15:43:47 +01:00
Alex Auvolat fe2dc5d51c Merge branch 'main' into next-0.10 2024-03-07 14:00:34 +01:00
Yureka c1769bbe69 ReplicationMode -> ConsistencyMode+ReplicationFactor 2024-03-07 12:45:33 +01:00
Yureka 8f86af52ed adjust docs for replication factor 2024-03-05 22:57:08 +01:00
Alex 603604cdfc Merge pull request 'refactor: remove max_write_errors and max_faults' (#760) from yuka/garage:remove-max-write-errors into next-0.10
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/760
2024-03-05 21:56:17 +00:00
Yureka 6760895926 refactor: remove max_write_errors and max_faults 2024-03-04 18:39:56 +01:00
Alex Auvolat bbde9bc912 Merge branch 'main' into next-0.10 2024-03-04 15:56:10 +01:00
Alex Auvolat d0d95fd53f [next-0.10] woodpecker: run debug pipeline on manual trigger 2024-02-27 10:13:09 +01:00
Alex Auvolat 4b978b7533 Merge branch 'main' into next-0.10 2024-02-26 18:55:24 +01:00
Alex Auvolat 3692af7052 Merge branch 'main' into next-0.10 2024-02-23 18:28:05 +01:00
Alex Auvolat 916c67ccf4 Merge branch 'main' into next-0.10 2024-02-23 16:50:34 +01:00
Alex Auvolat 81cebdd124 [next-0.10] fix build 2024-02-22 15:53:47 +01:00
Alex Auvolat 59f61c966a Merge branch 'main' into next-0.10 2024-02-22 15:45:45 +01:00
Alex Auvolat 75e591727d [next-0.10] cluster node status metrics: report nodes of all active layout versions 2024-02-20 17:08:31 +01:00
Alex Auvolat 643d1aabd8 Merge branch 'main' into next-0.10 2024-02-20 17:02:44 +01:00
Alex Auvolat eb4a6ce106 Merge branch 'main' into next-0.10 2024-02-15 14:06:34 +01:00
Alex Auvolat cf2af186fc Merge branch 'main' into next-0.10 2024-02-13 11:36:28 +01:00
Alex Auvolat db48dd3d6c bump crate versions to 0.10.0 2024-01-11 12:05:51 +01:00
Alex 8a6ec1d611 Merge pull request 'NLnet task 3' (#667) from nlnet-task3 into next-0.10
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/667
2024-01-11 10:58:08 +00:00
Alex Auvolat 0041b013a4 layout: refactoring and fix in layout helper 2023-12-11 16:09:22 +01:00
Alex Auvolat adccce1145 layout: refactor/fix bad while loop 2023-12-11 15:45:14 +01:00
Alex Auvolat 85b5a6bcd1 fix some clippy lints 2023-12-11 15:31:47 +01:00
Alex Auvolat e4f493b481 table: remove redundant tracing in insert_many 2023-12-11 14:57:42 +01:00
Alex Auvolat f8df90b79b table: fix insert_many to not send duplicates 2023-12-08 14:54:11 +01:00
Alex Auvolat 4dbf254512 layout: refactoring, merge two files 2023-12-08 14:15:52 +01:00
Alex Auvolat 64a6e557a4 rpc helper: small refactorings 2023-12-08 12:18:12 +01:00
Alex Auvolat 5dd200c015 layout: move block_read_nodes_of to rpc_helper to avoid double-locking
(in theory, this could have caused a deadlock)
2023-12-08 12:02:24 +01:00
Alex Auvolat 063294dd56 layout version: refactor get_node_zone 2023-12-08 11:50:58 +01:00
Alex Auvolat 7f2541101f cli: improvements to the layout commands when multiple layouts are live 2023-12-08 11:24:23 +01:00
Alex Auvolat 91b874c4ef rpc: fix system::health 2023-12-08 10:36:37 +01:00
Alex Auvolat 431b28e0cf fix build with discovery features 2023-12-07 15:15:59 +01:00
Alex Auvolat 9cecea64d4 layout: allow sync update tracker to progress with only quorums 2023-12-07 14:51:20 +01:00
Alex Auvolat aa59059a91 layout cli: safer skip-dead-nodes command 2023-12-07 11:56:14 +01:00
Alex Auvolat d90de365b3 table sync: use write quorums to report global success or failure of sync 2023-12-07 11:16:10 +01:00
Alex Auvolat 95eb13eb08 rpc: refactor result tracking for quorum sets 2023-12-07 10:57:21 +01:00
Alex Auvolat c8356a91d9 layout updates: fix the set of nodes among which minima are calculated 2023-12-07 10:30:26 +01:00
Alex Auvolat c04dd8788a admin: more info in admin GetClusterStatus 2023-11-28 14:25:04 +01:00
Alex Auvolat 539af6eac4 rpc helper: write comments + small refactoring of tracing 2023-11-28 11:12:39 +01:00
Alex Auvolat c539077d30 cli: remove historic layout info from status 2023-11-27 16:22:27 +01:00
Alex Auvolat 11e6fef93c cli: add layout history and layout assume-sync commands 2023-11-27 16:22:25 +01:00
Alex Auvolat 539a920313 cli: show when nodes are draining metadata 2023-11-27 13:18:59 +01:00
Alex Auvolat 78362140f5 rpc: update system::health to take into account write sets for all partitions 2023-11-27 12:10:21 +01:00
Alex Auvolat d6d239fc79 block manager: read_block using old layout versions if necessary 2023-11-27 11:52:57 +01:00
Alex Auvolat 3ecd14b9f6 table: implement write sets for insert_many 2023-11-16 16:41:45 +01:00
Alex Auvolat 22f38808e7 rpc_helper: don't use tokio::spawn for individual requests 2023-11-16 16:34:01 +01:00
Alex Auvolat 707442f5de layout: refactor digests and add "!=" assertions before epidemic bcast 2023-11-16 13:51:40 +01:00
Alex Auvolat ad5c6f779f layout: split helper in separate file; more precise difference tracking 2023-11-16 13:26:43 +01:00
Alex Auvolat d4df03424f layout: fix test 2023-11-15 15:56:57 +01:00
Alex Auvolat 33c8a489b0 layou: implement ack locking 2023-11-15 15:40:44 +01:00
Alex Auvolat 393c4d4515 layout: add helper for cached/external values to centralize recomputation 2023-11-15 14:20:50 +01:00
Alex Auvolat 65066c7064 layout: wip cache global mins 2023-11-15 13:28:30 +01:00
Alex Auvolat acd49de9f9 rpc: fix write set quorums 2023-11-15 13:07:42 +01:00
Alex Auvolat 46007bf01d integration test: print stdout and stderr on subcommand crash 2023-11-15 12:56:52 +01:00
Alex Auvolat b3e729f4b8 layout history merge: rm invalid versions when valid versions are added 2023-11-15 12:15:58 +01:00
Alex Auvolat 7ef2c23120 layout: fix test 2023-11-14 15:45:01 +01:00
Alex Auvolat 90e1619b1e table: take into account multiple write sets in inserts 2023-11-14 15:40:46 +01:00
Alex Auvolat 3b361d2959 layout: prepare for write sets 2023-11-14 14:28:16 +01:00
Alex Auvolat 866196750f system: add todo wrt new layout 2023-11-14 13:36:58 +01:00
Alex Auvolat 83a11374ca layout: fixes in schema 2023-11-14 13:29:26 +01:00
Alex Auvolat 1aab1f4e68 layout: refactoring of all_nodes 2023-11-14 13:12:32 +01:00
Alex Auvolat 8e292e06b3 layout: some refactoring of nongateway nodes 2023-11-14 12:48:38 +01:00
Alex Auvolat 9a491fa137 layout: fix test 2023-11-11 13:10:59 +01:00
Alex Auvolat df24bb806d layout/sync: fix bugs and add tracing 2023-11-11 12:44:27 +01:00
Alex Auvolat ce89d1ddab table sync: adapt to new layout history 2023-11-11 12:08:32 +01:00
Alex Auvolat df36cf3099 layout: add helpers to LayoutHistory and prepare integration with Table 2023-11-09 16:32:31 +01:00
Alex Auvolat 9d95f6f704 layout: fix tracker bugs 2023-11-09 15:52:45 +01:00
Alex Auvolat bad7cc812e layout admin: add missing calls to update_hash 2023-11-09 15:42:10 +01:00
Alex Auvolat 03ebf18830 layout: begin managing the update tracker values 2023-11-09 15:31:59 +01:00
Alex Auvolat 94caf9c0c1 layout: separate code path for synchronizing update trackers only 2023-11-09 14:53:34 +01:00
Alex Auvolat bfb1845fdc layout: refactor to use a RwLock on LayoutHistory 2023-11-09 14:12:05 +01:00
Alex Auvolat 19ef1ec8e7 layout: more refactoring 2023-11-09 13:34:14 +01:00
Alex Auvolat 8a2b1dd422 wip: split out layout management from System into separate LayoutManager 2023-11-09 12:55:36 +01:00
Alex Auvolat 523d2ecb95 layout: use separate CRDT for staged layout changes 2023-11-09 11:19:43 +01:00
Alex Auvolat 1da0a5676e bump garage protocol version tag to 0x000A (0.10) 2023-11-08 19:30:58 +01:00
Alex Auvolat 8dccee3ccf cluster layout: adapt all uses of ClusterLayout to LayoutHistory 2023-11-08 19:28:36 +01:00
Alex Auvolat fe9af1dcaa WIP: garage_rpc: store layout version history 2023-11-08 17:49:06 +01:00
Alex Auvolat 4a9c94514f avoid using layout_watch in System directly 2023-11-08 16:41:00 +01:00
Alex Auvolat 12d1dbfc6b remove Ring and use ClusterLayout everywhere 2023-11-08 15:41:24 +01:00
Alex Auvolat 0962313ebd garage_rpc: reorder functions in layout.rs 2023-11-08 13:13:04 +01:00
266 changed files with 27726 additions and 18219 deletions
-3
View File
@@ -1,3 +0,0 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
+11 -23
View File
@@ -5,6 +5,7 @@ when:
- pull_request
- deployment
- cron
- manual
steps:
- name: check formatting
@@ -15,34 +16,21 @@ steps:
- name: build
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
- nix-build -j4 --attr flakePackages.dev
- name: unit + func tests
- name: unit + func tests (lmdb)
image: nixpkgs/nix:nixos-22.05
environment:
GARAGE_TEST_INTEGRATION_EXE: result-bin/bin/garage
GARAGE_TEST_INTEGRATION_PATH: tmp-garage-integration
commands:
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
- nix-build --no-build-output --attr test.amd64
- ./result/bin/garage_db-*
- ./result/bin/garage_api-*
- ./result/bin/garage_model-*
- ./result/bin/garage_rpc-*
- ./result/bin/garage_table-*
- ./result/bin/garage_util-*
- ./result/bin/garage_web-*
- ./result/bin/garage-*
- GARAGE_TEST_INTEGRATION_DB_ENGINE=sled ./result/bin/integration-* || (cat tmp-garage-integration/stderr.log; false)
- nix-shell --attr ci --run "killall -9 garage" || true
- GARAGE_TEST_INTEGRATION_DB_ENGINE=lmdb ./result/bin/integration-* || (cat tmp-garage-integration/stderr.log; false)
- nix-shell --attr ci --run "killall -9 garage" || true
- GARAGE_TEST_INTEGRATION_DB_ENGINE=sqlite ./result/bin/integration-* || (cat tmp-garage-integration/stderr.log; false)
- rm result
- rm -rv tmp-garage-integration
- nix-build -j4 --attr flakePackages.tests-lmdb
- name: unit + func tests (sqlite)
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build -j4 --attr flakePackages.tests-sqlite
- name: integration tests
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build --no-build-output --attr clippy.amd64 --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
- nix-build -j4 --attr flakePackages.dev
- nix-shell --attr ci --run ./script/test-smoke.sh || (cat /tmp/garage.log; false)
depends_on: [ build ]
+8 -7
View File
@@ -9,19 +9,20 @@ depends_on:
steps:
- name: refresh-index
image: nixpkgs/nix:nixos-22.05
secrets:
- source: garagehq_aws_access_key_id
target: AWS_ACCESS_KEY_ID
- source: garagehq_aws_secret_access_key
target: AWS_SECRET_ACCESS_KEY
environment:
AWS_ACCESS_KEY_ID:
from_secret: garagehq_aws_access_key_id
AWS_SECRET_ACCESS_KEY:
from_secret: garagehq_aws_secret_access_key
commands:
- mkdir -p /etc/nix && cp nix/nix.conf /etc/nix/nix.conf
- nix-shell --attr ci --run "refresh_index"
- name: multiarch-docker
image: nixpkgs/nix:nixos-22.05
secrets:
- docker_auth
environment:
DOCKER_AUTH:
from_secret: docker_auth
commands:
- mkdir -p /root/.docker
- echo $DOCKER_AUTH > /root/.docker/config.json
+17 -11
View File
@@ -18,13 +18,12 @@ steps:
- name: build
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build --no-build-output --attr pkgs.${ARCH}.release --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
- nix-build --attr releasePackages.${ARCH} --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
- name: check is static binary
image: nixpkgs/nix:nixos-22.05
commands:
- nix-build --no-build-output --attr pkgs.${ARCH}.release --argstr git_version ${CI_COMMIT_TAG:-$CI_COMMIT_SHA}
- nix-shell --attr ci --run "./script/not-dynamic.sh result-bin/bin/garage"
- nix-shell --attr ci --run "./script/not-dynamic.sh result/bin/garage"
- name: integration tests
image: nixpkgs/nix:nixos-22.05
@@ -36,7 +35,15 @@ steps:
- matrix:
ARCH: i386
- name: upgrade tests
- name: upgrade tests from v1.0.0
image: nixpkgs/nix:nixos-22.05
commands:
- nix-shell --attr ci --run "./script/test-upgrade.sh v1.0.0 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
when:
- matrix:
ARCH: amd64
- name: upgrade tests from v0.8.4
image: nixpkgs/nix:nixos-22.05
commands:
- nix-shell --attr ci --run "./script/test-upgrade.sh v0.8.4 x86_64-unknown-linux-musl" || (cat /tmp/garage.log; false)
@@ -48,11 +55,10 @@ steps:
image: nixpkgs/nix:nixos-22.05
environment:
TARGET: "${TARGET}"
secrets:
- source: garagehq_aws_access_key_id
target: AWS_ACCESS_KEY_ID
- source: garagehq_aws_secret_access_key
target: AWS_SECRET_ACCESS_KEY
AWS_ACCESS_KEY_ID:
from_secret: garagehq_aws_access_key_id
AWS_SECRET_ACCESS_KEY:
from_secret: garagehq_aws_secret_access_key
commands:
- nix-shell --attr ci --run "to_s3"
@@ -61,8 +67,8 @@ steps:
environment:
DOCKER_PLATFORM: "linux/${ARCH}"
CONTAINER_NAME: "dxflrs/${ARCH}_garage"
secrets:
- docker_auth
DOCKER_AUTH:
from_secret: docker_auth
commands:
- mkdir -p /root/.docker
- echo $DOCKER_AUTH > /root/.docker/config.json
Generated
+1442 -1070
View File
File diff suppressed because it is too large Load Diff
-7027
View File
File diff suppressed because it is too large Load Diff
+29 -21
View File
@@ -8,7 +8,10 @@ members = [
"src/table",
"src/block",
"src/model",
"src/api",
"src/api/common",
"src/api/s3",
"src/api/k2v",
"src/api/admin",
"src/web",
"src/garage",
"src/k2v-client",
@@ -21,15 +24,18 @@ default-members = ["src/garage"]
# Internal Garage crates
format_table = { version = "0.1.1", path = "src/format-table" }
garage_api = { version = "0.9.4", path = "src/api" }
garage_block = { version = "0.9.4", path = "src/block" }
garage_db = { version = "0.9.4", path = "src/db", default-features = false }
garage_model = { version = "0.9.4", path = "src/model", default-features = false }
garage_net = { version = "0.9.4", path = "src/net" }
garage_rpc = { version = "0.9.4", path = "src/rpc" }
garage_table = { version = "0.9.4", path = "src/table" }
garage_util = { version = "0.9.4", path = "src/util" }
garage_web = { version = "0.9.4", path = "src/web" }
garage_api_common = { version = "2.0.0", path = "src/api/common" }
garage_api_admin = { version = "2.0.0", path = "src/api/admin" }
garage_api_s3 = { version = "2.0.0", path = "src/api/s3" }
garage_api_k2v = { version = "2.0.0", path = "src/api/k2v" }
garage_block = { version = "2.0.0", path = "src/block" }
garage_db = { version = "2.0.0", path = "src/db", default-features = false }
garage_model = { version = "2.0.0", path = "src/model", default-features = false }
garage_net = { version = "2.0.0", path = "src/net" }
garage_rpc = { version = "2.0.0", path = "src/rpc" }
garage_table = { version = "2.0.0", path = "src/table" }
garage_util = { version = "2.0.0", path = "src/util" }
garage_web = { version = "2.0.0", path = "src/web" }
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
# External crates from crates.io
@@ -42,30 +48,33 @@ blake2 = "0.10"
bytes = "1.0"
bytesize = "1.1"
cfg-if = "1.0"
chrono = "0.4"
chrono = { version = "0.4", features = ["serde"] }
crc-fast = "1.1"
crypto-common = "0.1"
digest = "0.10"
err-derive = "0.3"
gethostname = "0.4"
git-version = "0.3.4"
hex = "0.4"
hexdump = "0.1"
hmac = "0.12"
idna = "0.5"
itertools = "0.12"
ipnet = "2.9.0"
lazy_static = "1.4"
md-5 = "0.10"
mktemp = "0.5"
nix = { version = "0.27", default-features = false, features = ["fs"] }
nix = { version = "0.29", default-features = false, features = ["fs"] }
nom = "7.1"
parse_duration = "2.1"
paste = "1.0"
pin-project = "1.0.12"
pnet_datalink = "0.34"
rand = "0.8"
sha1 = "0.10"
sha2 = "0.10"
timeago = { version = "0.4", default-features = false }
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
aes-gcm = { version = "0.10", features = ["aes", "stream"] }
sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" }
kuska-handshake = { version = "0.2.0", features = ["default", "async_std"] }
@@ -80,7 +89,6 @@ heed = { version = "0.11", default-features = false, features = ["lmdb"] }
rusqlite = "0.31.0"
r2d2 = "0.8"
r2d2_sqlite = "0.24"
sled = "0.34"
async-compression = { version = "0.4", features = ["tokio", "zstd"] }
zstd = { version = "0.13", default-features = false }
@@ -91,6 +99,7 @@ serde = { version = "1.0", default-features = false, features = ["derive", "rc"]
serde_bytes = "0.11"
serde_json = "1.0"
toml = { version = "0.8", default-features = false, features = ["parse"] }
utoipa = { version = "5.3.1", features = ["chrono"] }
# newer version requires rust edition 2021
k8s-openapi = { version = "0.21", features = ["v1_24"] }
@@ -123,8 +132,8 @@ opentelemetry-contrib = "0.9"
prometheus = "0.13"
# used by the k2v-client crate only
aws-sigv4 = { version = "1.1" }
hyper-rustls = { version = "0.26", features = ["http2"] }
aws-sigv4 = { version = "1.1", default-features = false }
hyper-rustls = { version = "0.26", default-features = false, features = ["http1", "http2", "ring", "rustls-native-certs"] }
log = "0.4"
thiserror = "1.0"
@@ -132,10 +141,9 @@ thiserror = "1.0"
assert-json-diff = "2.0"
rustc_version = "0.4.0"
static_init = "1.0"
aws-config = "1.1.4"
aws-sdk-config = "1.13"
aws-sdk-s3 = "1.14"
aws-smithy-runtime = { version = "1.8", default-features = false, features = ["tls-rustls"] }
aws-sdk-config = { version = "1.62", default-features = false }
aws-sdk-s3 = { version = "1.79", default-features = false, features = ["rt-tokio"] }
[profile.dev]
#lto = "thin" # disabled for now, adds 2-4 min to each CI build
+1 -1
View File
@@ -3,5 +3,5 @@ FROM scratch
ENV RUST_BACKTRACE=1
ENV RUST_LOG=garage=info
COPY result-bin/bin/garage /
COPY result/bin/garage /
CMD [ "/garage", "server"]
+3 -8
View File
@@ -1,13 +1,8 @@
.PHONY: doc all release shell run1 run2 run3
.PHONY: doc all run1 run2 run3
all:
clear; cargo build
release:
nix-build --attr pkgs.amd64.release --no-build-output
shell:
nix-shell
clear
cargo build
# ----
+13 -45
View File
@@ -3,54 +3,22 @@
with import ./nix/common.nix;
let
pkgs = import pkgsSrc { };
pkgs = import nixpkgs { };
compile = import ./nix/compile.nix;
build_debug_and_release = (target: {
debug = (compile {
inherit system target git_version pkgsSrc cargo2nixOverlay;
release = false;
}).workspace.garage { compileMode = "build"; };
release = (compile {
inherit system target git_version pkgsSrc cargo2nixOverlay;
release = true;
}).workspace.garage { compileMode = "build"; };
});
test = (rustPkgs:
pkgs.symlinkJoin {
name = "garage-tests";
paths =
builtins.map (key: rustPkgs.workspace.${key} { compileMode = "test"; })
(builtins.attrNames rustPkgs.workspace);
});
build_release = target: (compile {
inherit target system git_version nixpkgs;
crane = flake.inputs.crane;
rust-overlay = flake.inputs.rust-overlay;
release = true;
}).garage;
in {
pkgs = {
amd64 = build_debug_and_release "x86_64-unknown-linux-musl";
i386 = build_debug_and_release "i686-unknown-linux-musl";
arm64 = build_debug_and_release "aarch64-unknown-linux-musl";
arm = build_debug_and_release "armv6l-unknown-linux-musleabihf";
};
test = {
amd64 = test (compile {
inherit system git_version pkgsSrc cargo2nixOverlay;
target = "x86_64-unknown-linux-musl";
features = [
"garage/bundled-libs"
"garage/k2v"
"garage/sled"
"garage/lmdb"
"garage/sqlite"
];
});
};
clippy = {
amd64 = (compile {
inherit system git_version pkgsSrc cargo2nixOverlay;
target = "x86_64-unknown-linux-musl";
compiler = "clippy";
}).workspace.garage { compileMode = "build"; };
releasePackages = {
amd64 = build_release "x86_64-unknown-linux-musl";
i386 = build_release "i686-unknown-linux-musl";
arm64 = build_release "aarch64-unknown-linux-musl";
arm = build_release "armv6l-unknown-linux-musleabihf";
};
flakePackages = flake.packages.${system};
}
+1 -1
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Garage Adminstration API v0</title>
<title>Garage administration API v0</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
+1 -1
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Garage Adminstration API v0</title>
<title>Garage administration API v1</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
+2 -3
View File
@@ -98,7 +98,6 @@ paths:
type: string
example:
- "k2v"
- "sled"
- "lmdb"
- "sqlite"
- "consul-discovery"
@@ -688,7 +687,7 @@ paths:
operationId: "GetBucketInfo"
summary: "Get a bucket"
description: |
Given a bucket identifier (`id`) or a global alias (`alias`), get its information.
Given a bucket identifier (`id`) or a global alias (`globalAlias`), get its information.
It includes its aliases, its web configuration, keys that have some permissions
on it, some statistics (number of objects, size), number of dangling multipart uploads,
and its quotas (if any).
@@ -702,7 +701,7 @@ paths:
example: "b4018dc61b27ccb5c64ec1b24f53454bbbd180697c758c4d47a22a8921864a87"
schema:
type: string
- name: alias
- name: globalAlias
in: query
description: |
The exact global alias of one of the existing buckets.
+24
View File
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>Garage administration API v2</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="./css/redoc.css" rel="stylesheet">
<!--
Redoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='./garage-admin-v2.json'></redoc>
<script src="./redoc.standalone.js"> </script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -23,7 +23,7 @@ client = minio.Minio(
"GKyourapikey",
"abcd[...]1234",
# Force the region, this is specific to garage
region="region",
region="garage",
)
```
+53 -7
View File
@@ -69,7 +69,7 @@ $CONFIG = array(
'hostname' => '127.0.0.1', // Can also be a domain name, eg. garage.example.com
'port' => 3900, // Put your reverse proxy port or your S3 API port
'use_ssl' => false, // Set it to true if you have a TLS enabled reverse proxy
'region' => 'garage', // Garage has only one region named "garage"
'region' => 'garage', // Garage default region is named "garage", edit according to your cluster config
'use_path_style' => true // Garage supports only path style, must be set to true
],
],
@@ -80,6 +80,53 @@ To test your new configuration, just reload your Nextcloud webpage and start sen
*External link:* [Nextcloud Documentation > Primary Storage](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/primary_storage.html)
#### SSE-C encryption (since Garage v1.0)
Since version 1.0, Garage supports server-side encryption with customer keys
(SSE-C). In this mode, Garage is responsible for encrypting and decrypting
objects, but it does not store the encryption key itself. The encryption key
should be provided by Nextcloud upon each request. This mode of operation is
supported by Nextcloud and it has successfully been tested together with
Garage.
To enable SSE-C encryption:
1. Make sure your Garage server is accessible via SSL through a reverse proxy
such as Nginx, and that it is using a valid public certificate (Nextcloud
might be able to connect to an S3 server that is using a self-signed
certificate, but you will lose many hours while trying, so don't).
Configure values for `use_ssl` and `port` accordingly in your `config.php`
file.
2. Generate an encryption key using the following command:
```
openssl rand -base64 32
```
Make sure to keep this key **secret**!
3. Add the encryption key in your `config.php` file as follows:
```php
<?php
$CONFIG = array(
'objectstore' => [
'class' => '\\OC\\Files\\ObjectStore\\S3',
'arguments' => [
...
'sse_c_key' => 'exampleencryptionkeyLbU+5fKYQcVoqnn+RaIOXgo=',
...
],
],
```
Nextcloud will now make Garage encrypt files at rest in the storage bucket.
These files will not be readable by an S3 client that has credentials to the
bucket but doesn't also know the secret encryption key.
### External Storage
**From the GUI.** Activate the "External storage support" app from the "Applications" page (click on your account icon on the top right corner of your screen to display the menu). Go to your parameters page (also located below your account icon). Click on external storage (or the corresponding translation in your language).
@@ -88,7 +135,7 @@ To test your new configuration, just reload your Nextcloud webpage and start sen
*Click on the picture to zoom*
Add a new external storage. Put what you want in "folder name" (eg. "shared"). Select "Amazon S3". Keep "Access Key" for the Authentication field.
In Configuration, put your bucket name (eg. nextcloud), the host (eg. 127.0.0.1), the port (eg. 3900 or 443), the region (garage). Tick the SSL box if you have put an HTTPS proxy in front of garage. You must tick the "Path access" box and you must leave the "Legacy authentication (v2)" box empty. Put your Key ID (eg. GK...) and your Secret Key in the last two input boxes. Finally click on the tick symbol on the right of your screen.
In Configuration, put your bucket name (eg. nextcloud), the host (eg. 127.0.0.1), the port (eg. 3900 or 443), the region ("garage" if you use the default, or the one your configured in your `garage.toml`). Tick the SSL box if you have put an HTTPS proxy in front of garage. You must tick the "Path access" box and you must leave the "Legacy authentication (v2)" box empty. Put your Key ID (eg. GK...) and your Secret Key in the last two input boxes. Finally click on the tick symbol on the right of your screen.
Now go to your "Files" app and a new "linked folder" has appeared with the name you chose earlier (eg. "shared").
@@ -191,7 +238,7 @@ object_storage:
# Put localhost only if you have a garage instance running on that node
endpoint: 'http://localhost:3900' # or "garage.example.com" if you have TLS on port 443
# Garage supports only one region for now, named garage
# Garage default region is named "garage", edit according to your config
region: 'garage'
credentials:
@@ -245,7 +292,7 @@ with average object size ranging from 50 KB to 150 KB.
As such, your Garage cluster should be configured appropriately for good performance:
- use Garage v0.8.0 or higher with the [LMDB database engine](@documentation/reference-manual/configuration.md#db-engine-since-v0-8-0).
With the default Sled database engine, your database could quickly end up taking tens of GB of disk space.
Older versions of Garage used the Sled database engine which had issues, such as databases quickly ending up taking tens of GB of disk space.
- the Garage database should be stored on a SSD
### Creating your bucket
@@ -288,6 +335,7 @@ From the [official Mastodon documentation](https://docs.joinmastodon.org/admin/t
```bash
$ RAILS_ENV=production bin/tootctl media remove --days 3
$ RAILS_ENV=production bin/tootctl media remove --days 15 --prune-profiles
$ RAILS_ENV=production bin/tootctl media remove-orphans
$ RAILS_ENV=production bin/tootctl preview_cards remove --days 15
```
@@ -306,8 +354,6 @@ Imports: 1.7 KB
Settings: 0 Bytes
```
Unfortunately, [old avatars and headers cannot currently be cleaned up](https://github.com/mastodon/mastodon/issues/9567).
### Migrating your data
Data migration should be done with an efficient S3 client.
@@ -395,7 +441,7 @@ media_storage_providers:
store_synchronous: True # do we want to wait that the file has been written before returning?
config:
bucket: matrix # the name of our bucket, we chose matrix earlier
region_name: garage # only "garage" is supported for the region field
region_name: garage # "garage" by default, edit according to your cluster config
endpoint_url: http://localhost:3900 # the path to the S3 endpoint
access_key_id: "GKxxx" # your Key ID
secret_access_key: "xxxx" # your Secret Key
+1 -1
View File
@@ -17,7 +17,7 @@ Garage can also help you serve this content.
## Gitea
You can use Garage with Gitea to store your [git LFS](https://git-lfs.github.com/) data, your users' avatar, and their attachements.
You can use Garage with Gitea to store your [git LFS](https://git-lfs.github.com/) data, your users' avatar, and their attachments.
You can configure a different target for each data type (check `[lfs]` and `[attachment]` sections of the Gitea documentation) and you can provide a default one through the `[storage]` section.
Let's start by creating a key and a bucket (your key id and secret will be needed later, keep them somewhere):
+35 -12
View File
@@ -53,20 +53,43 @@ and that's also why your nodes have super long identifiers.
Adding TLS support built into Garage is not currently planned.
## Garage stores data in plain text on the filesystem
## Garage stores data in plain text on the filesystem or encrypted using customer keys (SSE-C)
Garage does not handle data encryption at rest by itself, and instead delegates
to the user to add encryption, either at the storage layer (LUKS, etc) or on
the client side (or both). There are no current plans to add data encryption
directly in Garage.
For standard S3 API requests, Garage does not encrypt data at rest by itself.
For the most generic at rest encryption of data, we recommend setting up your
storage partitions on encrypted LUKS devices.
Implementing data encryption directly in Garage might make things simpler for
end users, but also raises many more questions, especially around key
management: for encryption of data, where could Garage get the encryption keys
from ? If we encrypt data but keep the keys in a plaintext file next to them,
it's useless. We probably don't want to have to manage secrets in garage as it
would be very hard to do in a secure way. Maybe integrate with an external
system such as Hashicorp Vault?
If you are developping your own client software that makes use of S3 storage,
we recommend implementing data encryption directly on the client side and never
transmitting plaintext data to Garage. This makes it easy to use an external
untrusted storage provider if necessary.
Garage does support [SSE-C
encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html),
an encryption mode of Amazon S3 where data is encrypted at rest using
encryption keys given by the client. The encryption keys are passed to the
server in a header in each request, to encrypt or decrypt data at the moment of
reading or writing. The server discards the key as soon as it has finished
using it for the request. This mode allows the data to be encrypted at rest by
Garage itself, but it requires support in the client software. It is also not
adapted to a model where the server is not trusted or assumed to be
compromised, as the server can easily know the encryption keys. Note however
that when using SSE-C encryption, the only Garage node that knows the
encryption key passed in a given request is the node to which the request is
directed (which can be a gateway node), so it is easy to have untrusted nodes
in the cluster as long as S3 API requests containing SSE-C encryption keys are
not directed to them.
Implementing automatic data encryption directly in Garage without client-side
management of keys (something like
[SSE-S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html))
could make things simpler for end users that don't want to setup LUKS, but also
raises many more questions, especially around key management: for encryption of
data, where could Garage get the encryption keys from? If we encrypt data but
keep the keys in a plaintext file next to them, it's useless. We probably don't
want to have to manage secrets in Garage as it would be very hard to do in a
secure way. At the time of speaking, there are no plans to implement this in
Garage.
# Adding data encryption using external tools
-1
View File
@@ -91,6 +91,5 @@ The following feature flags are available in v0.8.0:
| `metrics` | *by default* | Enable collection of metrics in Prometheus format on the admin API |
| `telemetry-otlp` | optional | Enable collection of execution traces using OpenTelemetry |
| `syslog` | optional | Enable logging to Syslog |
| `sled` | *by default* | Enable using Sled to store Garage's metadata |
| `lmdb` | *by default* | Enable using LMDB to store Garage's metadata |
| `sqlite` | *by default* | Enable using Sqlite3 to store Garage's metadata |
+67 -1
View File
@@ -26,6 +26,13 @@ Or deploy with custom values:
helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml
```
If you want to manage the CustomRessourceDefinition used by garage for its `kubernetes_discovery` outside of the helm chart, add `garage.kubernetesSkipCrd: true` to your custom values and use the kustomization before deploying the helm chart:
```bash
kubectl apply -k ../k8s/crd
helm install --create-namespace --namespace garage garage ./garage -f values.override.yaml
```
After deploying, cluster layout must be configured manually as described in [Creating a cluster layout](@/documentation/quick-start/_index.md#creating-a-cluster-layout). Use the following command to access garage CLI:
```bash
@@ -45,7 +52,7 @@ This is an example `values.overrride.yaml` for deploying in a microk8s cluster w
```yaml
garage:
# Use only 2 replicas per object
replicationMode: "2"
replicationFactor: 2
# Start 4 instances (StatefulSets) of garage
deployment:
@@ -86,3 +93,62 @@ helm delete --namespace garage garage
```
Note that this will leave behind custom CRD `garagenodes.deuxfleurs.fr`, which must be removed manually if desired.
## Increase PVC size on running Garage instances
Since the Garage Helm chart creates the data and meta PVC based on `StatefulSet` templates, increasing the PVC size can be a bit tricky.
### Confirm the `StorageClass` used for Garage supports volume expansion
Confirm the storage class used for garage.
```bash
kubectl -n garage get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
data-garage-0 Bound pvc-080360c9-8ce3-4acf-8579-1701e57b7f3f 30Gi RWO longhorn-local <unset> 77d
data-garage-1 Bound pvc-ab8ba697-6030-4fc7-ab3c-0d6df9e3dbc0 30Gi RWO longhorn-local <unset> 5d8h
data-garage-2 Bound pvc-3ab37551-0231-4604-986d-136d0fd950ec 30Gi RWO longhorn-local <unset> 5d5h
meta-garage-0 Bound pvc-3b457302-3023-4169-846e-c928c5f2ea65 3Gi RWO longhorn-local <unset> 77d
meta-garage-1 Bound pvc-49ace2b9-5c85-42df-9247-51c4cf64b460 3Gi RWO longhorn-local <unset> 5d8h
meta-garage-2 Bound pvc-99e2e50f-42b4-4128-ae2f-b52629259723 3Gi RWO longhorn-local <unset> 5d5h
```
In this case, the storage class is `longhorn-local`. Now, check if `ALLOWVOLUMEEXPANSION` is true for the used `StorageClass`.
```bash
kubectl get storageclasses.storage.k8s.io longhorn-local
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
longhorn-local driver.longhorn.io Delete Immediate true 103d
```
If your `StorageClass` does not support volume expansion, double check if you can enable it. Otherwise, your only real option is to spin up a new Garage cluster with increased size and migrate all data over.
If your `StorageClass` supports expansion, you are free to continue.
### Increase the size of the PVCs
Increase the size of all PVCs to your desired size.
```bash
kubectl -n garage edit pvc data-garage-0
kubectl -n garage edit pvc data-garage-1
kubectl -n garage edit pvc data-garage-2
kubectl -n garage edit pvc meta-garage-0
kubectl -n garage edit pvc meta-garage-1
kubectl -n garage edit pvc meta-garage-2
```
### Increase the size of the `StatefulSet` PVC template
This is an optional step, but if not done, future instances of Garage will be created with the original size from the template.
```bash
kubectl -n garage delete sts --cascade=orphan garage
statefulset.apps "garage" deleted
```
This will remove the Garage `StatefulSet` but leave the pods running. It may seem destructive but needs to be done this way since edits to the size of PVC templates are prohibited.
### Redeploy the `StatefulSet`
Now the size of future PVCs can be increased, and the Garage Helm chart can be upgraded. The new `StatefulSet` should take ownership of the orphaned pods again.
+11 -8
View File
@@ -89,20 +89,21 @@ to store 2 TB of data in total.
- If you only have an HDD and no SSD, it's fine to put your metadata alongside
the data on the same drive, but then consider your filesystem choice wisely
(see above). Having lots of RAM for your kernel to cache the metadata will
help a lot with performance.
(see above). Having lots of RAM for your kernel to cache the metadata will
help a lot with performance. The default LMDB database engine is the most
tested and has good performance.
## Get a Docker image
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
We encourage you to use a fixed tag (eg. `v0.9.4`) and not the `latest` tag.
For this example, we will use the latest published version at the time of the writing which is `v0.9.4` but it's up to you
We encourage you to use a fixed tag (eg. `v2.0.0`) and not the `latest` tag.
For this example, we will use the latest published version at the time of the writing which is `v2.0.0` but it's up to you
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
For example:
```
sudo docker pull dxflrs/garage:v0.9.4
sudo docker pull dxflrs/garage:v2.0.0
```
## Deploying and configuring Garage
@@ -127,7 +128,7 @@ data_dir = "/var/lib/garage/data"
db_engine = "lmdb"
metadata_auto_snapshot_interval = "6h"
replication_mode = "3"
replication_factor = 3
compression_level = 2
@@ -151,6 +152,8 @@ Check the following for your configuration files:
- Make sure `rpc_public_addr` contains the public IP address of the node you are configuring.
This parameter is optional but recommended: if your nodes have trouble communicating with
one another, consider adding it.
Alternatively, you can also set `rpc_public_addr_subnet`, which can filter
the addresses announced to other peers to a specific subnet.
- Make sure `rpc_secret` is the same value on all nodes. It should be a 32-bytes hex-encoded secret key.
You can generate such a key with `openssl rand -hex 32`.
@@ -168,7 +171,7 @@ docker run \
-v /etc/garage.toml:/etc/garage.toml \
-v /var/lib/garage/meta:/var/lib/garage/meta \
-v /var/lib/garage/data:/var/lib/garage/data \
dxflrs/garage:v0.9.4
dxflrs/garage:v2.0.0
```
With this command line, Garage should be started automatically at each boot.
@@ -182,7 +185,7 @@ If you want to use `docker-compose`, you may use the following `docker-compose.y
version: "3"
services:
garage:
image: dxflrs/garage:v0.9.4
image: dxflrs/garage:v2.0.0
network_mode: "host"
restart: unless-stopped
volumes:
+1
View File
@@ -28,6 +28,7 @@ StateDirectory=garage
DynamicUser=true
ProtectHome=true
NoNewPrivileges=true
LimitNOFILE=42000
[Install]
WantedBy=multi-user.target
+17
View File
@@ -50,3 +50,20 @@ locations. They use Garage themselves for the following tasks:
The Deuxfleurs Garage cluster is a multi-site cluster currently composed of
9 nodes in 3 physical locations.
### Triplebit
[Triplebit](https://www.triplebit.org) is a non-profit hosting provider and
ISP focused on improving access to privacy-related services. They use
Garage themselves for the following tasks:
- Hosting of their homepage, [privacyguides.org](https://www.privacyguides.org/), and various other static sites
- As a Mastodon object storage backend for [mstdn.party](https://mstdn.party/) and [mstdn.plus](https://mstdn.plus/)
- As a PeerTube storage backend for [neat.tube](https://neat.tube/)
- As a [Matrix media backend](https://github.com/matrix-org/synapse-s3-storage-provider)
Triplebit's Garage cluster is a multi-site cluster currently composed of
10 nodes in 3 physical locations.
+1 -1
View File
@@ -97,7 +97,7 @@ delete a tombstone, the following condition has to be met:
superseeded by the tombstone. This ensures that deleting the tombstone is
safe and that no deleted value will come back in the system.
Garage makes use of Sled's atomic operations (such as compare-and-swap and
Garage uses atomic database operations (such as compare-and-swap and
transactions) to ensure that only tombstones that have been correctly
propagated to other nodes are ever deleted from the local entry tree.
+1 -1
View File
@@ -67,7 +67,7 @@ Pithos has been abandonned and should probably not used yet, in the following we
Pithos was relying as a S3 proxy in front of Cassandra (and was working with Scylla DB too).
From its designers' mouth, storing data in Cassandra has shown its limitations justifying the project abandonment.
They built a closed-source version 2 that does not store blobs in the database (only metadata) but did not communicate further on it.
We considered there v2's design but concluded that it does not fit both our *Self-contained & lightweight* and *Simple* properties. It makes the development, the deployment and the operations more complicated while reducing the flexibility.
We considered their v2's design but concluded that it does not fit both our *Self-contained & lightweight* and *Simple* properties. It makes the development, the deployment and the operations more complicated while reducing the flexibility.
**[Riak CS](https://docs.riak.com/riak/cs/2.1.1/index.html):**
*Not written yet*
+3 -3
View File
@@ -36,7 +36,7 @@ sudo killall nix-daemon
Now you can enter our nix-shell, all the required packages will be downloaded but they will not pollute your environment outside of the shell:
```bash
nix-shell
nix-shell -A devShell
```
You can use the traditional Rust development workflow:
@@ -65,8 +65,8 @@ nix-build -j $(nproc) --max-jobs auto
```
Our build has multiple parameters you might want to set:
- `release` build with release optimisations instead of debug
- `target allows` for cross compilation
- `release` to build with release optimisations instead of debug
- `target` allows for cross compilation
- `compileMode` can be set to test or bench to build a unit test runner
- `git_version` to inject the hash to display when running `garage stats`
+4 -1
View File
@@ -141,4 +141,7 @@ blocks may still be held by Garage. If you suspect that such corruption has occu
in your cluster, you can run one of the following repair procedures:
- `garage repair versions`: checks that all versions belong to a non-deleted object, and purges any orphan version
- `garage repair block_refs`: checks that all block references belong to a non-deleted object version, and purges any orphan block reference (this will then allow the blocks to be garbage-collected)
- `garage repair block-refs`: checks that all block references belong to a non-deleted object version, and purges any orphan block reference (this will then allow the blocks to be garbage-collected)
- `garage repair block-rc`: checks that the reference counters for blocks are in sync with the actual number of non-deleted entries in the block reference table
+1 -1
View File
@@ -12,7 +12,7 @@ An introduction to building cluster layouts can be found in the [production depl
In Garage, all of the data that can be stored in a given cluster is divided
into slices which we call *partitions*. Each partition is stored by
one or several nodes in the cluster
(see [`replication_mode`](@/documentation/reference-manual/configuration.md#replication_mode)).
(see [`replication_factor`](@/documentation/reference-manual/configuration.md#replication_factor)).
The layout determines the correspondence between these partitions,
which exist on a logical level, and actual storage nodes.
+2 -2
View File
@@ -21,14 +21,14 @@ data_dir = [
```
Garage will automatically balance all blocks stored by the node
among the different specified directories, proportionnally to the
among the different specified directories, proportionally to the
specified capacities.
## Updating the list of storage locations
If you add new storage locations to your `data_dir`,
Garage will not rebalance existing data between storage locations.
Newly written blocks will be balanced proportionnally to the specified capacities,
Newly written blocks will be balanced proportionally to the specified capacities,
and existing data may be moved between drives to improve balancing,
but only opportunistically when a data block is re-written (e.g. an object
is re-uploaded, or an object with a duplicate block is uploaded).
+2 -2
View File
@@ -5,7 +5,7 @@ weight = 40
Garage is meant to work on old, second-hand hardware.
In particular, this makes it likely that some of your drives will fail, and some manual intervention will be needed.
Fear not! For Garage is fully equipped to handle drive failures, in most common cases.
Fear not! Garage is fully equipped to handle drive failures, in most common cases.
## A note on availability of Garage
@@ -61,7 +61,7 @@ garage repair -a --yes blocks
This will re-synchronize blocks of data that are missing to the new HDD, reading them from copies located on other nodes.
You can check on the advancement of this process by doing the following command:
You can check on the advancement of this process by doing the following command:
```bash
garage stats -a
+1 -1
View File
@@ -71,7 +71,7 @@ The entire procedure would look something like this:
2. Take each node offline individually to back up its metadata folder, bring them back online once the backup is done.
You can do all of the nodes in a single zone at once as that won't impact global cluster availability.
Do not try to make a backup of the metadata folder of a running node.
Do not try to manually copy the metadata folder of a running node.
**Since Garage v0.9.4,** you can use the `garage meta snapshot --all` command
to take a simultaneous snapshot of the metadata database files of all your
+36 -3
View File
@@ -42,6 +42,13 @@ If a binary of the last version is not available for your architecture,
or if you want a build customized for your system,
you can [build Garage from source](@/documentation/cookbook/from-source.md).
If none of these option work for you, you can also run Garage in a Docker
container. When using Docker, the commands used in this guide will not work
anymore. We recommend reading the tutorial on [configuring a
multi-node cluster](@/documentation/cookbook/real-world.md) to learn about
using Garage as a Docker container. For simplicity, a minimal command to launch
Garage using Docker is provided in this quick start guide as well.
## Configuring and starting Garage
@@ -59,7 +66,7 @@ metadata_dir = "/tmp/meta"
data_dir = "/tmp/data"
db_engine = "sqlite"
replication_mode = "none"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
@@ -85,6 +92,9 @@ metrics_token = "$(openssl rand -base64 32)"
EOF
```
See the [Configuration file format](https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/)
for complete options and values.
Now that your configuration file has been created, you may save it to the directory of your choice.
By default, Garage looks for **`/etc/garage.toml`.**
You can also store it somewhere else, but you will have to specify `-c path/to/garage.toml`
@@ -111,6 +121,26 @@ garage -c path/to/garage.toml server
If you have placed the `garage.toml` file in `/etc` (its default location), you can simply run `garage server`.
Alternatively, if you cannot or do not wish to run the Garage binary directly,
you may use Docker to run Garage in a container using the following command:
```bash
docker run \
-d \
--name garaged \
-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903 \
-v /path/to/garage.toml:/etc/garage.toml \
-v /path/to/garage/meta:/var/lib/garage/meta \
-v /path/to/garage/data:/var/lib/garage/data \
dxflrs/garage:v2.0.0
```
Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`
#### Troubleshooting
Ensure your configuration file, `metadata_dir` and `data_dir` are readable by the user running the `garage` server or Docker.
You can tune Garage's verbosity by setting the `RUST_LOG=` environment variable. \
Available log levels are (from less verbose to more verbose): `error`, `warn`, `info` *(default)*, `debug` and `trace`.
@@ -131,6 +161,9 @@ It uses values from the TOML configuration file to find the Garage daemon runnin
local node, therefore if your configuration file is not at `/etc/garage.toml` you will
again have to specify `-c path/to/garage.toml` at each invocation.
If you are running Garage in a Docker container, you can set `alias garage="docker exec -ti <container name> /garage"`
to use the Garage binary inside your container.
If the `garage` CLI is able to correctly detect the parameters of your local Garage node,
the following command should be enough to show the status of your cluster:
@@ -166,7 +199,7 @@ For instance here you could write just `garage layout assign -z dc1 -c 1G 563e`.
The layout then has to be applied to the cluster, using:
```bash
garage layout apply
garage layout apply --version 1
```
@@ -316,7 +349,7 @@ Check [our s3 compatibility list](@/documentation/reference-manual/s3-compatibil
### Other tools for interacting with Garage
The following tools can also be used to send and recieve files from/to Garage:
The following tools can also be used to send and receive files from/to Garage:
- [minio-client](@/documentation/connect/cli.md#minio-client)
- [s3cmd](@/documentation/connect/cli.md#s3cmd)
+189 -108
View File
@@ -8,13 +8,16 @@ weight = 20
Here is an example `garage.toml` configuration file that illustrates all of the possible options:
```toml
replication_mode = "3"
replication_factor = 3
consistency_mode = "consistent"
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
metadata_snapshots_dir = "/var/lib/garage/snapshots"
metadata_fsync = true
data_fsync = false
disable_scrub = false
use_local_tz = false
metadata_auto_snapshot_interval = "6h"
db_engine = "lmdb"
@@ -22,8 +25,6 @@ db_engine = "lmdb"
block_size = "1M"
block_ram_buffer_max = "256MiB"
sled_cache_capacity = "128MiB"
sled_flush_every_ms = 2000
lmdb_map_size = "1T"
compression_level = 1
@@ -32,6 +33,9 @@ rpc_secret = "4425f5c26c5e11581d3223904324dcb5b5d5dfb14e5e7f35e38c595424f5f1e6"
rpc_bind_addr = "[::]:3901"
rpc_bind_outgoing = false
rpc_public_addr = "[fc00:1::1]:3901"
# or set rpc_public_adr_subnet to filter down autodiscovery to a subnet:
# rpc_public_addr_subnet = "2001:0db8:f00:b00:/64"
allow_world_readable_secrets = false
@@ -42,6 +46,7 @@ bootstrap_peers = [
"212fd62eeaca72c122b45a7f4fa0f55e012aa5e24ac384a72a3016413fa724ff@[fc00:F::1]:3901",
]
allow_punycode = false
[consul_discovery]
api = "catalog"
@@ -71,10 +76,12 @@ root_domain = ".s3.garage"
[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.garage"
add_host_to_metrics = true
[admin]
api_bind_addr = "0.0.0.0:3903"
metrics_token = "BCAdFjoa9G0KJR0WXnHHm7fs1ZAbfpI8iIZ+Z/a2NgI="
metrics_require_token = true
admin_token = "UkLeGWEvHnXBqnueR3ISEMWpOnm40jH2tM2HnnL/0F4="
trace_sink = "http://localhost:4317"
```
@@ -97,17 +104,20 @@ Top-level configuration options:
[`data_fsync`](#data_fsync),
[`db_engine`](#db_engine),
[`disable_scrub`](#disable_scrub),
[`use_local_tz`](#use_local_tz),
[`lmdb_map_size`](#lmdb_map_size),
[`metadata_auto_snapshot_interval`](#metadata_auto_snapshot_interval),
[`metadata_dir`](#metadata_dir),
[`metadata_fsync`](#metadata_fsync),
[`replication_mode`](#replication_mode),
[`metadata_snapshots_dir`](#metadata_snapshots_dir),
[`replication_factor`](#replication_factor),
[`consistency_mode`](#consistency_mode),
[`rpc_bind_addr`](#rpc_bind_addr),
[`rpc_bind_outgoing`](#rpc_bind_outgoing),
[`rpc_public_addr`](#rpc_public_addr),
[`rpc_secret`/`rpc_secret_file`](#rpc_secret),
[`sled_cache_capacity`](#sled_cache_capacity),
[`sled_flush_every_ms`](#sled_flush_every_ms).
[`rpc_public_addr_subnet`](#rpc_public_addr_subnet)
[`rpc_secret`/`rpc_secret_file`](#rpc_secret).
[`allow_punycode`](#allow_punycode).
The `[consul_discovery]` section:
[`api`](#consul_api),
@@ -132,11 +142,13 @@ The `[s3_api]` section:
[`s3_region`](#s3_region).
The `[s3_web]` section:
[`add_host_to_metrics`](#web_add_host_to_metrics),
[`bind_addr`](#web_bind_addr),
[`root_domain`](#web_root_domain).
The `[admin]` section:
[`api_bind_addr`](#admin_api_bind_addr),
[`metrics_require_token`](#admin_metrics_require_token),
[`metrics_token`/`metrics_token_file`](#admin_metrics_token),
[`admin_token`/`admin_token_file`](#admin_token),
[`trace_sink`](#admin_trace_sink),
@@ -146,7 +158,7 @@ The `[admin]` section:
The following configuration parameter must be specified as an environment
variable, it does not exist in the configuration file:
- `GARAGE_LOG_TO_SYSLOG` (since v0.9.4): set this to `1` or `true` to make the
- `GARAGE_LOG_TO_SYSLOG` (since `v0.9.4`): set this to `1` or `true` to make the
Garage daemon send its logs to `syslog` (using the libc `syslog` function)
instead of printing to stderr.
@@ -161,11 +173,12 @@ values in the configuration file:
### Top-level configuration options
#### `replication_mode` {#replication_mode}
#### `replication_factor` {#replication_factor}
Garage supports the following replication modes:
The replication factor can be any positive integer smaller or equal the node count in your cluster.
The chosen replication factor has a big impact on the cluster's failure tolerancy and performance characteristics.
- `none` or `1`: data stored on Garage is stored on a single node. There is no
- `1`: data stored on Garage is stored on a single node. There is no
redundancy, and data will be unavailable as soon as one node fails or its
network is disconnected. Do not use this for anything else than test
deployments.
@@ -176,17 +189,6 @@ Garage supports the following replication modes:
before losing data. Data remains available in read-only mode when one node is
down, but write operations will fail.
- `2-dangerous`: a variant of mode `2`, where written objects are written to
the second replica asynchronously. This means that Garage will return `200
OK` to a PutObject request before the second copy is fully written (or even
before it even starts being written). This means that data can more easily
be lost if the node crashes before a second copy can be completed. This
also means that written objects might not be visible immediately in read
operations. In other words, this mode severely breaks the consistency and
durability guarantees of standard Garage cluster operation. Benefits of
this mode: you can still write to your cluster when one node is
unavailable.
- `3`: data stored on Garage will be stored on three different nodes, if
possible each in a different zones. Garage tolerates two node failure, or
several node failures but in no more than two zones (in a deployment with at
@@ -194,55 +196,84 @@ Garage supports the following replication modes:
or node failures are only in a single zone, reading and writing data to
Garage can continue normally.
- `3-degraded`: a variant of replication mode `3`, that lowers the read
quorum to `1`, to allow you to read data from your cluster when several
nodes (or nodes in several zones) are unavailable. In this mode, Garage
does not provide read-after-write consistency anymore. The write quorum is
still 2, ensuring that data successfully written to Garage is stored on at
least two nodes.
- `3-dangerous`: a variant of replication mode `3` that lowers both the read
and write quorums to `1`, to allow you to both read and write to your
cluster when several nodes (or nodes in several zones) are unavailable. It
is the least consistent mode of operation proposed by Garage, and also one
that should probably never be used.
- `5`, `7`, ...: When setting the replication factor above 3, it is most useful to
choose an uneven value, since for every two copies added, one more node can fail
before losing the ability to write and read to the cluster.
Note that in modes `2` and `3`,
if at least the same number of zones are available, an arbitrary number of failures in
any given zone is tolerated as copies of data will be spread over several zones.
**Make sure `replication_mode` is the same in the configuration files of all nodes.
**Make sure `replication_factor` is the same in the configuration files of all nodes.
Never run a Garage cluster where that is not the case.**
It is technically possible to change the replication factor although it's a
dangerous operation that is not officially supported. This requires you to
delete the existing cluster layout and create a new layout from scratch,
meaning that a full rebalancing of your cluster's data will be needed. To do
it, shut down your cluster entirely, delete the `custer_layout` files in the
meta directories of all your nodes, update all your configuration files with
the new `replication_factor` parameter, restart your cluster, and then create a
new layout with all the nodes you want to keep. Rebalancing data will take
some time, and data might temporarily appear unavailable to your users.
It is recommended to shut down public access to the cluster while rebalancing
is in progress. In theory, no data should be lost as rebalancing is a
routine operation for Garage, although we cannot guarantee you that everything
will go right in such an extreme scenario.
#### `consistency_mode` {#consistency_mode}
The consistency mode setting determines the read and write behaviour of your cluster.
- `consistent`: The default setting. This is what the paragraph above describes.
The read and write quorum will be determined so that read-after-write consistency
is guaranteed.
- `degraded`: Lowers the read
quorum to `1`, to allow you to read data from your cluster when several
nodes (or nodes in several zones) are unavailable. In this mode, Garage
does not provide read-after-write consistency anymore.
The write quorum stays the same as in the `consistent` mode, ensuring that
data successfully written to Garage is stored on multiple nodes (depending
the replication factor).
- `dangerous`: This mode lowers both the read
and write quorums to `1`, to allow you to both read and write to your
cluster when several nodes (or nodes in several zones) are unavailable. It
is the least consistent mode of operation proposed by Garage, and also one
that should probably never be used.
Changing the `consistency_mode` between modes while leaving the `replication_factor` untouched
(e.g. setting your node's `consistency_mode` to `degraded` when it was previously unset, or from
`dangerous` to `consistent`), can be done easily by just changing the `consistency_mode`
parameter in your config files and restarting all your Garage nodes.
The consistency mode can be used together with various replication factors, to achieve
a wide range of read and write characteristics. Some examples:
- Replication factor `2`, consistency mode `degraded`: While this mode
technically exists, its properties are the same as with consistency mode `consistent`,
since the read quorum with replication factor `2`, consistency mode `consistent` is already 1.
- Replication factor `2`, consistency mode `dangerous`: written objects are written to
the second replica asynchronously. This means that Garage will return `200
OK` to a PutObject request before the second copy is fully written (or even
before it even starts being written). This means that data can more easily
be lost if the node crashes before a second copy can be completed. This
also means that written objects might not be visible immediately in read
operations. In other words, this configuration severely breaks the consistency and
durability guarantees of standard Garage cluster operation. Benefits of
this configuration: you can still write to your cluster when one node is
unavailable.
The quorums associated with each replication mode are described below:
| `replication_mode` | Number of replicas | Write quorum | Read quorum | Read-after-write consistency? |
| ------------------ | ------------------ | ------------ | ----------- | ----------------------------- |
| `none` or `1` | 1 | 1 | 1 | yes |
| `2` | 2 | 2 | 1 | yes |
| `2-dangerous` | 2 | 1 | 1 | NO |
| `3` | 3 | 2 | 2 | yes |
| `3-degraded` | 3 | 2 | 1 | NO |
| `3-dangerous` | 3 | 1 | 1 | NO |
Changing the `replication_mode` between modes with the same number of replicas
(e.g. from `3` to `3-degraded`, or from `2-dangerous` to `2`), can be done easily by
just changing the `replication_mode` parameter in your config files and restarting all your
Garage nodes.
It is also technically possible to change the replication mode to a mode with a
different numbers of replicas, although it's a dangerous operation that is not
officially supported. This requires you to delete the existing cluster layout
and create a new layout from scratch, meaning that a full rebalancing of your
cluster's data will be needed. To do it, shut down your cluster entirely,
delete the `custer_layout` files in the meta directories of all your nodes,
update all your configuration files with the new `replication_mode` parameter,
restart your cluster, and then create a new layout with all the nodes you want
to keep. Rebalancing data will take some time, and data might temporarily
appear unavailable to your users. It is recommended to shut down public access
to the cluster while rebalancing is in progress. In theory, no data should be
lost as rebalancing is a routine operation for Garage, although we cannot
guarantee you that everything will go right in such an extreme scenario.
| `consistency_mode` | `replication_factor` | Write quorum | Read quorum | Read-after-write consistency? |
| ------------------ | -------------------- | ------------ | ----------- | ----------------------------- |
| `consistent` | 1 | 1 | 1 | yes |
| `consistent` | 2 | 2 | 1 | yes |
| `dangerous` | 2 | 1 | 1 | NO |
| `consistent` | 3 | 2 | 2 | yes |
| `degraded` | 3 | 2 | 1 | NO |
| `dangerous` | 3 | 1 | 1 | NO |
#### `metadata_dir` {#metadata_dir}
@@ -252,6 +283,7 @@ as the index of all objects, object version and object blocks.
Store this folder on a fast SSD drive if possible to maximize Garage's performance.
#### `data_dir` {#data_dir}
The directory in which Garage will store the data blocks of objects.
@@ -272,29 +304,43 @@ data_dir = [
See [the dedicated documentation page](@/documentation/operations/multi-hdd.md)
on how to operate Garage in such a setup.
#### `metadata_snapshots_dir` (since `v1.1.0`) {#metadata_snapshots_dir}
The directory in which Garage will store metadata snapshots when it
performs a snapshot of the metadata database, either when instructed to do
so from a RPC call or regularly through
[`metadata_auto_snapshot_interval`](#metadata_auto_snapshot_interval).
By default, Garage will store snapshots into a `snapshots/` subdirectory
of [`metadata_dir`](#metadata_dir). This might quickly fill up your
metadata storage space if you use snapshots, because Garage will need up
to 4x the space of the existing metadata database: each snapshot requires
roughly as much space as the original database, and Garage temporarily
needs to store up to three different snapshots before it cleans up the oldest
snapshot to go back to two stored snapshots.
To prevent filling your disk, you might to change this setting to a
directory with ample available space, e.g. on the same storage space as
[`data_dir`](#data_dir).
#### `db_engine` (since `v0.8.0`) {#db_engine}
Since `v0.8.0`, Garage can use alternative storage backends as follows:
| DB engine | `db_engine` value | Database path |
| --------- | ----------------- | ------------- |
| [LMDB](https://www.lmdb.tech) (default since `v0.9.0`) | `"lmdb"` | `<metadata_dir>/db.lmdb/` |
| [Sled](https://sled.rs) (default up to `v0.8.0`) | `"sled"` | `<metadata_dir>/db/` |
| [Sqlite](https://sqlite.org) | `"sqlite"` | `<metadata_dir>/db.sqlite` |
| [LMDB](https://www.symas.com/lmdb) (since `v0.8.0`, default since `v0.9.0`) | `"lmdb"` | `<metadata_dir>/db.lmdb/` |
| [Sqlite](https://sqlite.org) (since `v0.8.0`) | `"sqlite"` | `<metadata_dir>/db.sqlite` |
| [Sled](https://sled.rs) (old default, removed since `v1.0`) | `"sled"` | `<metadata_dir>/db/` |
Sled was the only database engine up to Garage v0.7.0. Performance issues and
API limitations of Sled prompted the addition of alternative engines in v0.8.0.
Since v0.9.0, LMDB is the default engine instead of Sled, and Sled is
deprecated. We plan to remove Sled in Garage v1.0.
Sled was supported until Garage v0.9.x, and was removed in Garage v1.0.
You can still use an older binary of Garage (e.g. v0.9.4) to migrate
old Sled metadata databases to another engine.
Performance characteristics of the different DB engines are as follows:
- Sled: tends to produce large data files and also has performance issues,
especially when the metadata folder is on a traditional HDD and not on SSD.
- LMDB: the recommended database engine for high-performance distributed
clusters, much more space-efficient and significantly faster. LMDB works very
well, but is known to have the following limitations:
- LMDB: the recommended database engine for high-performance distributed clusters.
LMDB works very well, but is known to have the following limitations:
- 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
@@ -310,6 +356,9 @@ Performance characteristics of the different DB engines are as follows:
other nodes), or if you have saved regular snapshots at the filesystem
level.
- Keys in LMDB are limited to 511 bytes. This limit translates to limits on
object keys in S3 and sort keys in K2V that are limted to 479 bytes.
- Sqlite: Garage supports Sqlite as an alternative storage backend for
metadata, which does not have the issues listed above for LMDB.
On versions 0.8.x and earlier, Sqlite should be avoided due to abysmal
@@ -353,7 +402,6 @@ Here is how this option impacts the different database engines:
| Database | `metadata_fsync = false` (default) | `metadata_fsync = true` |
|----------|------------------------------------|-------------------------------|
| Sled | default options | *unsupported* |
| Sqlite | `PRAGMA synchronous = OFF` | `PRAGMA synchronous = NORMAL` |
| LMDB | `MDB_NOMETASYNC` + `MDB_NOSYNC` | `MDB_NOMETASYNC` |
@@ -372,7 +420,7 @@ at the cost of a moderate drop in write performance.
Similarly to `metatada_fsync`, this is likely not necessary
if geographical replication is used.
#### `metadata_auto_snapshot_interval` (since Garage v0.9.4) {#metadata_auto_snapshot_interval}
#### `metadata_auto_snapshot_interval` (since `v0.9.4`) {#metadata_auto_snapshot_interval}
If this value is set, Garage will automatically take a snapshot of the metadata
DB file at a regular interval and save it in the metadata directory.
@@ -400,7 +448,7 @@ month, with a random delay to avoid all nodes running at the same time. When
it scrubs the data directory, Garage will read all of the data files stored on
disk to check their integrity, and will rebuild any data files that it finds
corrupted, using the remaining valid copies stored on other nodes.
See [this page](@/documentation/operations/durability-repair.md#scrub) for details.
See [this page](@/documentation/operations/durability-repairs.md#scrub) for details.
Set the `disable_scrub` configuration value to `true` if you don't need Garage
to scrub the data directory, for instance if you are already scrubbing at the
@@ -409,6 +457,13 @@ you should delete it from the data directory and then call `garage repair
blocks` on the node to ensure that it re-obtains a copy from another node on
the network.
#### `use_local_tz` (since `v1.1.0`) {#use_local_tz}
By default, Garage runs the lifecycle worker every day at midnight in UTC. Set the
`use_local_tz` configuration value to `true` if you want Garage to run the
lifecycle worker at midnight in your local timezone. If you have multiple nodes,
you should also ensure that each node has the same timezone configuration.
#### `block_size` {#block_size}
Garage splits stored objects in consecutive chunks of size `block_size`
@@ -424,7 +479,7 @@ files will remain available. This however means that chunks from existing files
will not be deduplicated with chunks from newly uploaded files, meaning you
might use more storage space that is optimally possible.
#### `block_ram_buffer_max` (since v0.9.4) {#block_ram_buffer_max}
#### `block_ram_buffer_max` (since `v0.9.4`) {#block_ram_buffer_max}
A limit on the total size of data blocks kept in RAM by S3 API nodes awaiting
to be sent to storage nodes asynchronously.
@@ -455,21 +510,6 @@ node.
The default value is 256MiB.
#### `sled_cache_capacity` {#sled_cache_capacity}
This parameter can be used to tune the capacity of the cache used by
[sled](https://sled.rs), the database Garage uses internally to store metadata.
Tune this to fit the RAM you wish to make available to your Garage instance.
This value has a conservative default (128MB) so that Garage doesn't use too much
RAM by default, but feel free to increase this for higher performance.
#### `sled_flush_every_ms` {#sled_flush_every_ms}
This parameters can be used to tune the flushing interval of sled.
Increase this if sled is thrashing your SSD, at the risk of losing more data in case
of a power outage (though this should not matter much as data is replicated on other
nodes). The default value, 2000ms, should be appropriate for most use cases.
#### `lmdb_map_size` {#lmdb_map_size}
This parameters can be used to set the map size used by LMDB,
@@ -526,7 +566,7 @@ the node, even in the case of a NAT: the NAT should be configured to forward the
port number to the same internal port nubmer. This means that if you have several nodes running
behind a NAT, they should each use a different RPC port number.
#### `rpc_bind_outgoing`(since v0.9.2) {#rpc_bind_outgoing}
#### `rpc_bind_outgoing` (since `v0.9.2`) {#rpc_bind_outgoing}
If enabled, pre-bind all sockets for outgoing connections to the same IP address
used for listening (the IP address specified in `rpc_bind_addr`) before
@@ -544,6 +584,14 @@ RPC calls. **This parameter is optional but recommended.** In case you have
a NAT that binds the RPC port to a port that is different on your public IP,
this field might help making it work.
#### `rpc_public_addr_subnet` {#rpc_public_addr_subnet}
In case `rpc_public_addr` is not set, but autodiscovery is used, this allows
filtering the list of automatically discovered IPs to a specific subnet.
For example, if nodes should pick *their* IP inside a specific subnet, but you
don't want to explicitly write the IP down (as it's dynamic, or you want to
share configs across nodes), you can use this option.
#### `bootstrap_peers` {#bootstrap_peers}
A list of peer identifiers on which to contact other Garage peers of this cluster.
@@ -560,7 +608,7 @@ be obtained by running `garage node id` and then included directly in the
key will be returned by `garage node id` and you will have to add the IP
yourself.
### `allow_world_readable_secrets` or `GARAGE_ALLOW_WORLD_READABLE_SECRETS` (env) {#allow_world_readable_secrets}
#### `allow_world_readable_secrets` or `GARAGE_ALLOW_WORLD_READABLE_SECRETS` (env) {#allow_world_readable_secrets}
Garage checks the permissions of your secret files to make sure they're not
world-readable. In some cases, the check might fail and consider your files as
@@ -572,6 +620,13 @@ permission verification.
Alternatively, you can set the `GARAGE_ALLOW_WORLD_READABLE_SECRETS`
environment variable to `true` to bypass the permissions check.
#### `allow_punycode` {#allow_punycode}
Allow creating buckets with names containing punycode. When used for buckets served
as websites, this allows using almost any unicode character in the domain name.
Default to `false`.
### The `[consul_discovery]` section
Garage supports discovering other nodes of the cluster using Consul. For this
@@ -702,6 +757,13 @@ For instance, if `root_domain` is `web.garage.eu`, a bucket called `deuxfleurs.f
will be accessible either with hostname `deuxfleurs.fr.web.garage.eu`
or with hostname `deuxfleurs.fr`.
#### `add_host_to_metrics` {#web_add_host_to_metrics}
Whether to include the requested domain name (HTTP `Host` header) in the
Prometheus metrics of the web endpoint. This is disabled by default as the
number of possible values is not bounded and can be a source of cardinality
explosion in the exported metrics.
### The `[admin]` section
@@ -716,10 +778,34 @@ See [administration API reference](@/documentation/reference-manual/admin-api.md
Alternatively, since `v0.8.5`, a path can be used to create a unix socket. Note that for security reasons,
the socket will have 0220 mode. Make sure to set user and group permissions accordingly.
#### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
The token for accessing all administration functions on the admin endpoint,
with the exception of the metrics endpoint (see `metrics_token`).
You can use any random string for this value. We recommend generating a random
token with `openssl rand -base64 32`.
For Garage version earlier than `v2.0`, if this token is not set,
access to these endpoints is disabled entirely.
Since Garage `v2.0`, additional admin API tokens can be defined dynamically
in your Garage cluster using administration commands. This new admin token system
is more flexible since it allows admin tokens to have an expiration date,
and to have a scope restricted to certain admin API functions. If `admin_token`
is set, it behaves as an admin token without expiration and with full scope.
Otherwise, only admin API tokens defined dynamically can be used.
`admin_token` was introduced in Garage `v0.7.2`.
`admin_token_file` and the `GARAGE_ADMIN_TOKEN` environment variable are supported since Garage `v0.8.2`.
`GARAGE_ADMIN_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
#### `metrics_token`, `metrics_token_file` or `GARAGE_METRICS_TOKEN`, `GARAGE_METRICS_TOKEN_FILE` (env) {#admin_metrics_token}
The token for accessing the Metrics endpoint. If this token is not set, the
Metrics endpoint can be accessed without access control.
The token for accessing the Prometheus metrics endpoint (`/metrics`).
If this token is not set, and unless `metrics_require_token` is set to `true`,
the metrics endpoint can be accessed without access control.
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
@@ -728,17 +814,12 @@ You can use any random string for this value. We recommend generating a random t
`GARAGE_METRICS_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
#### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
#### `metrics_require_token` (since `v2.0.0`) {#admin_metrics_require_token}
The token for accessing all of the other administration endpoints. If this
token is not set, access to these endpoints is disabled entirely.
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
`admin_token` was introduced in Garage `v0.7.2`.
`admin_token_file` and the `GARAGE_ADMIN_TOKEN` environment variable are supported since Garage `v0.8.2`.
`GARAGE_ADMIN_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
If this is set to `true`, accessing the metrics endpoint will always require
an access token. Valid tokens include the `metrics_token` if it is set,
and admin API token defined dynamicaly in Garage which have
the `Metrics` endpoint in their scope.
#### `trace_sink` {#admin_trace_sink}
+4 -4
View File
@@ -39,10 +39,10 @@ Read about cluster layout management [here](@/documentation/operations/layout.md
### Several replication modes
Garage supports a variety of replication modes, with 1 copy, 2 copies or 3 copies of your data,
Garage supports a variety of replication modes, with configurable replica count,
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).
Read our reference page on [supported replication modes](@/documentation/reference-manual/configuration.md#replication_factor)
to select the replication mode best suited to your use case (hint: in most cases, `replication_factor = 3` is what you want).
### Compression and deduplication
@@ -61,7 +61,7 @@ directed to a Garage cluster can be handled independently of one another instead
of going through a central bottleneck (the leader node).
As a consequence, requests can be handled much faster, even in cases where latency
between cluster nodes is important (see our [benchmarks](@/documentation/design/benchmarks/index.md) for data on this).
This is particularly usefull when nodes are far from one another and talk to one other through standard Internet connections.
This is particularly useful when nodes are far from one another and talk to one other through standard Internet connections.
### Web server for static websites
+1 -1
View File
@@ -392,7 +392,7 @@ table_merkle_updater_todo_queue_length{table_name="block_ref"} 0
#### `table_sync_items_received`, `table_sync_items_sent` (counters)
Number of data items sent to/recieved from other nodes during resync procedures
Number of data items sent to/received from other nodes during resync procedures
```
table_sync_items_received{from="<remote node>",table_name="bucket_v2"} 3
@@ -33,6 +33,7 @@ Feel free to open a PR to suggest fixes this table. Minio is missing because the
| [URL path-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#path-style-access) (eg. `host.tld/bucket/key`) | ✅ Implemented | ✅ | ✅ | ❓| ✅ |
| [URL vhost-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access) URL (eg. `bucket.host.tld/key`) | ✅ Implemented | ❌| ✅| ✅ | ✅ |
| [Presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) | ✅ Implemented | ❌| ✅ | ✅ | ✅(❓) |
| [SSE-C encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html) | ✅ Implemented | ❓ | ✅ | ❌ | ✅ |
*Note:* OpenIO does not says if it supports presigned URLs. Because it is part
of signature v4 and they claim they support it without additional precisions,
+2 -2
View File
@@ -42,7 +42,7 @@ The general principle are similar, but details have not been updated.**
A version is defined by the existence of at least one entry in the blocks table for a certain version UUID.
We must keep the following invariant: if a version exists in the blocks table, it has to be referenced in the objects table.
We explicitly manage concurrent versions of an object: the version timestamp and version UUID columns are index columns, thus we may have several concurrent versions of an object.
Important: before deleting an older version from the objects table, we must make sure that we did a successfull delete of the blocks of that version from the blocks table.
Important: before deleting an older version from the objects table, we must make sure that we did a successful delete of the blocks of that version from the blocks table.
Thus, the workflow for reading an object is as follows:
@@ -95,7 +95,7 @@ Known issue: if someone is reading from a version that we want to delete and the
Usefull metadata:
- list of versions that reference this block in the Casandra table, so that we can do GC by checking in Cassandra that the lines still exist
- list of other nodes that we know have acknowledged a write of this block, usefull in the rebalancing algorithm
- list of other nodes that we know have acknowledged a write of this block, useful in the rebalancing algorithm
Write strategy: have a single thread that does all write IO so that it is serialized (or have several threads that manage independent parts of the hash space). When writing a blob, write it to a temporary file, close, then rename so that a concurrent read gets a consistent result (either not found or found with whole content).
+1 -1
View File
@@ -68,7 +68,7 @@ The migration steps are as follows:
5. Turn off Garage 0.3
6. Backup metadata folders if you can (i.e. if you have space to do it
somewhere). Backuping data folders could also be usefull but that's much
somewhere). Backuping data folders could also be useful but that's much
harder to do. If your filesystem supports snapshots, this could be a good
time to use them.
+77
View File
@@ -0,0 +1,77 @@
+++
title = "Migrating from 0.9 to 1.0"
weight = 11
+++
**This guide explains how to migrate to 1.0 if you have an existing 0.9 cluster.
We don't recommend trying to migrate to 1.0 directly from 0.8 or older.**
This migration procedure has been tested on several clusters without issues.
However, it is still a *critical procedure* that might cause issues.
**Make sure to back up all your data before attempting it!**
You might also want to read our [general documentation on upgrading Garage](@/documentation/operations/upgrading.md).
## Changes introduced in v1.0
The following are **breaking changes** in Garage v1.0 that require your attention when migrating:
- The Sled metadata db engine has been **removed**. If your cluster was still
using Sled, you will need to **use a Garage v0.9.x binary** to convert the
database using the `garage convert-db` subcommand. See
[here](@/documentation/reference-manual/configuration.md#db_engine) for the
details of the procedure.
The following syntax changes have been made to the configuration file:
- The `replication_mode` parameter has been split into two parameters:
[`replication_factor`](@/documentation/reference-manual/configuration.md#replication_factor)
and
[`consistency_mode`](@/documentation/reference-manual/configuration.md#consistency_mode).
The old syntax using `replication_mode` is still supported for legacy
reasons and can still be used.
- The parameters `sled_cache_capacity` and `sled_flush_every_ms` have been removed.
## Migration procedure
The migration to Garage v1.0 can be done with almost no downtime,
by restarting all nodes at once in the new version.
The migration steps are as follows:
1. Do a `garage repair --all-nodes --yes tables`, check the logs and check that
all data seems to be synced correctly between nodes. If you have time, do
additional `garage repair` procedures (`blocks`, `versions`, `block_refs`,
etc.)
2. Ensure you have a snapshot of your Garage installation that you can restore
to in case the upgrade goes wrong:
- If you are running Garage v0.9.4 or later, use the `garage meta snapshot
--all` to make a backup snapshot of the metadata directories of your nodes
for backup purposes, and save a copy of the following files in the
metadata directories of your nodes: `cluster_layout`, `data_layout`,
`node_key`, `node_key.pub`.
- If you are running a filesystem such as ZFS or BTRFS that support
snapshotting, you can create a filesystem-level snapshot to be used as a
restoration point if needed.
- In other cases, make a backup using the old procedure: turn off each node
individually; back up its metadata folder (for instance, use the following
command if your metadata directory is `/var/lib/garage/meta`: `cd
/var/lib/garage ; tar -acf meta-v0.9.tar.zst meta/`); turn it back on
again. This will allow you to take a backup of all nodes without
impacting global cluster availability. You can do all nodes of a single
zone at once as this does not impact the availability of Garage.
3. Prepare your updated binaries and configuration files for Garage v1.0
4. Shut down all v0.9 nodes simultaneously, and restart them all simultaneously
in v1.0. Use your favorite deployment tool (Ansible, Kubernetes, Nomad) to
achieve this as fast as possible. Garage v1.0 should be in a working state
as soon as enough nodes have started.
5. Monitor your cluster in the following hours to see if it works well under
your production load.
@@ -37,7 +37,7 @@ There are two reasons for this:
Reminder: rules of simplicity, concerning changes to Garage's source code.
Always question what we are doing.
Never do anything just because it looks nice or because we "think" it might be usefull at some later point but without knowing precisely why/when.
Never do anything just because it looks nice or because we "think" it might be useful at some later point but without knowing precisely why/when.
Only do things that make perfect sense in the context of what we currently know.
## References
+157 -125
View File
@@ -13,8 +13,12 @@ We will bump the version numbers prefixed to each API endpoint each time the syn
or semantics change, meaning that code that relies on these endpoints will break
when changes are introduced.
The Garage administration API was introduced in version 0.7.2, this document
does not apply to older versions of Garage.
The Garage administration API was introduced in version 0.7.2, and was
changed several times.
**THIS DOCUMENT IS DEPRECATED.** We now have an OpenAPI spec which is automatically generated
from Garage's source code and is always up-to-date. See `doc/api/garage-admin-v2.html`.
Text in this document is no longer kept in sync with the admin API's actual behavior.
## Access control
@@ -52,116 +56,118 @@ Returns an HTTP status 200 if the node is ready to answer user's requests,
and an HTTP status 503 (Service Unavailable) if there are some partitions
for which a quorum of nodes is not available.
A simple textual message is also returned in a body with content-type `text/plain`.
See `/v1/health` for an API that also returns JSON output.
See `/v2/GetClusterHealth` for an API that also returns JSON output.
### Other special endpoints
#### CheckDomain `GET /check?domain=<domain>`
Checks whether this Garage cluster serves a website for domain `<domain>`.
Returns HTTP 200 Ok if yes, or HTTP 4xx if no website is available for this domain.
### Cluster operations
#### GetClusterStatus `GET /v1/status`
#### GetClusterStatus `GET /v2/GetClusterStatus`
Returns the cluster's current status in JSON, including:
- ID of the node being queried and its version of the Garage daemon
- Live nodes
- Currently configured cluster layout
- Staged changes to the cluster layout
Example response body:
```json
{
"node": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
"garageVersion": "git:v0.9.0-dev",
"garageFeatures": [
"k2v",
"sled",
"lmdb",
"sqlite",
"metrics",
"bundled-libs"
],
"rustVersion": "1.68.0",
"dbEngine": "LMDB (using Heed crate)",
"knownNodes": [
"layoutVersion": 5,
"nodes": [
{
"id": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
"addr": "10.0.0.11:3901",
"id": "62b218d848e86a64f7fe1909735f29a4350547b54c4b204f91246a14eb0a1a8c",
"role": {
"id": "62b218d848e86a64f7fe1909735f29a4350547b54c4b204f91246a14eb0a1a8c",
"zone": "dc1",
"capacity": 100000000000,
"tags": []
},
"addr": "10.0.0.3:3901",
"hostname": "node3",
"isUp": true,
"lastSeenSecsAgo": 9,
"hostname": "node1"
"lastSeenSecsAgo": 12,
"draining": false,
"dataPartition": {
"available": 660270088192,
"total": 873862266880
},
"metadataPartition": {
"available": 660270088192,
"total": 873862266880
}
},
{
"id": "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff",
"addr": "10.0.0.12:3901",
"id": "a11c7cf18af297379eff8688360155fe68d9061654449ba0ce239252f5a7487f",
"role": null,
"addr": "10.0.0.2:3901",
"hostname": "node2",
"isUp": true,
"lastSeenSecsAgo": 1,
"hostname": "node2"
"lastSeenSecsAgo": 11,
"draining": true,
"dataPartition": {
"available": 660270088192,
"total": 873862266880
},
"metadataPartition": {
"available": 660270088192,
"total": 873862266880
}
},
{
"id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
"addr": "10.0.0.21:3901",
"id": "a235ac7695e0c54d7b403943025f57504d500fdcc5c3e42c71c5212faca040a2",
"role": {
"id": "a235ac7695e0c54d7b403943025f57504d500fdcc5c3e42c71c5212faca040a2",
"zone": "dc1",
"capacity": 100000000000,
"tags": []
},
"addr": "127.0.0.1:3904",
"hostname": "lindy",
"isUp": true,
"lastSeenSecsAgo": 7,
"hostname": "node3"
"lastSeenSecsAgo": 2,
"draining": false,
"dataPartition": {
"available": 660270088192,
"total": 873862266880
},
"metadataPartition": {
"available": 660270088192,
"total": 873862266880
}
},
{
"id": "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b",
"addr": "10.0.0.22:3901",
"id": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df",
"role": {
"id": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df",
"zone": "dc1",
"capacity": 100000000000,
"tags": []
},
"addr": "10.0.0.1:3901",
"hostname": "node1",
"isUp": true,
"lastSeenSecsAgo": 1,
"hostname": "node4"
"lastSeenSecsAgo": 3,
"draining": false,
"dataPartition": {
"available": 660270088192,
"total": 873862266880
},
"metadataPartition": {
"available": 660270088192,
"total": 873862266880
}
}
],
"layout": {
"version": 12,
"roles": [
{
"id": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
"zone": "dc1",
"capacity": 10737418240,
"tags": [
"node1"
]
},
{
"id": "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff",
"zone": "dc1",
"capacity": 10737418240,
"tags": [
"node2"
]
},
{
"id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
"zone": "dc2",
"capacity": 10737418240,
"tags": [
"node3"
]
}
],
"stagedRoleChanges": [
{
"id": "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b",
"remove": false,
"zone": "dc2",
"capacity": 10737418240,
"tags": [
"node4"
]
}
{
"id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
"remove": true,
"zone": null,
"capacity": null,
"tags": null,
}
]
}
]
}
```
#### GetClusterHealth `GET /v1/health`
#### GetClusterHealth `GET /v2/GetClusterHealth`
Returns the cluster's current health in JSON format, with the following variables:
@@ -194,7 +200,7 @@ Example response body:
}
```
#### ConnectClusterNodes `POST /v1/connect`
#### ConnectClusterNodes `POST /v2/ConnectClusterNodes`
Instructs this Garage node to connect to other Garage nodes at specified addresses.
@@ -224,7 +230,7 @@ Example response:
]
```
#### GetClusterLayout `GET /v1/layout`
#### GetClusterLayout `GET /v2/GetClusterLayout`
Returns the cluster's current layout in JSON, including:
@@ -285,7 +291,7 @@ Example response body:
}
```
#### UpdateClusterLayout `POST /v1/layout`
#### UpdateClusterLayout `POST /v2/UpdateClusterLayout`
Send modifications to the cluster layout. These modifications will
be included in the staged role changes, visible in subsequent calls
@@ -322,7 +328,7 @@ This returns the new cluster layout with the proposed staged changes,
as returned by GetClusterLayout.
#### ApplyClusterLayout `POST /v1/layout/apply`
#### ApplyClusterLayout `POST /v2/ApplyClusterLayout`
Applies to the cluster the layout changes currently registered as
staged layout changes.
@@ -342,23 +348,11 @@ existing layout in the cluster.
This returns the message describing all the calculations done to compute the new
layout, as well as the description of the layout as returned by GetClusterLayout.
#### RevertClusterLayout `POST /v1/layout/revert`
#### RevertClusterLayout `POST /v2/RevertClusterLayout`
Clears all of the staged layout changes.
Request body format:
```json
{
"version": 13
}
```
Reverting the staged changes is done by incrementing the version number
and clearing the contents of the staged change list.
Similarly to the CLI, the body must include the incremented
version number, which MUST be 1 + the value of the currently
existing layout in the cluster.
This requests contains an empty body.
This returns the new cluster layout with all changes reverted,
as returned by GetClusterLayout.
@@ -366,7 +360,7 @@ as returned by GetClusterLayout.
### Access key operations
#### ListKeys `GET /v1/key`
#### ListKeys `GET /v2/ListKeys`
Returns all API access keys in the cluster.
@@ -385,8 +379,8 @@ Example response:
]
```
#### GetKeyInfo `GET /v1/key?id=<acces key id>`
#### GetKeyInfo `GET /v1/key?search=<pattern>`
#### GetKeyInfo `GET /v2/GetKeyInfo?id=<acces key id>`
#### GetKeyInfo `GET /v2/GetKeyInfo?search=<pattern>`
Returns information about the requested API access key.
@@ -460,7 +454,7 @@ Example response:
}
```
#### CreateKey `POST /v1/key`
#### CreateKey `POST /v2/CreateKey`
Creates a new API access key.
@@ -475,7 +469,7 @@ Request body format:
This returns the key info, including the created secret key,
in the same format as the result of GetKeyInfo.
#### ImportKey `POST /v1/key/import`
#### ImportKey `POST /v2/ImportKey`
Imports an existing API key.
This will check that the imported key is in the valid format, i.e.
@@ -493,7 +487,7 @@ Request body format:
This returns the key info in the same format as the result of GetKeyInfo.
#### UpdateKey `POST /v1/key?id=<acces key id>`
#### UpdateKey `POST /v2/UpdateKey?id=<acces key id>`
Updates information about the specified API access key.
@@ -515,14 +509,14 @@ The possible flags in `allow` and `deny` are: `createBucket`.
This returns the key info in the same format as the result of GetKeyInfo.
#### DeleteKey `DELETE /v1/key?id=<acces key id>`
#### DeleteKey `POST /v2/DeleteKey?id=<acces key id>`
Deletes an API access key.
### Bucket operations
#### ListBuckets `GET /v1/bucket`
#### ListBuckets `GET /v2/ListBuckets`
Returns all storage buckets in the cluster.
@@ -564,8 +558,8 @@ Example response:
]
```
#### GetBucketInfo `GET /v1/bucket?id=<bucket id>`
#### GetBucketInfo `GET /v1/bucket?globalAlias=<alias>`
#### GetBucketInfo `GET /v2/GetBucketInfo?id=<bucket id>`
#### GetBucketInfo `GET /v2/GetBucketInfo?globalAlias=<alias>`
Returns information about the requested storage bucket.
@@ -608,7 +602,7 @@ Example response:
}
```
#### CreateBucket `POST /v1/bucket`
#### CreateBucket `POST /v2/CreateBucket`
Creates a new storage bucket.
@@ -648,7 +642,7 @@ or no alias at all.
Technically, you can also specify both `globalAlias` and `localAlias` and that would create
two aliases, but I don't see why you would want to do that.
#### UpdateBucket `PUT /v1/bucket?id=<bucket id>`
#### UpdateBucket `POST /v2/UpdateBucket?id=<bucket id>`
Updates configuration of the given bucket.
@@ -680,16 +674,38 @@ In `quotas`: new values of `maxSize` and `maxObjects` must both be specified, or
to remove the quotas. An absent value will be considered the same as a `null`. It is not possible
to change only one of the two quotas.
#### DeleteBucket `DELETE /v1/bucket?id=<bucket id>`
#### DeleteBucket `POST /v2/DeleteBucket?id=<bucket id>`
Deletes a storage bucket. A bucket cannot be deleted if it is not empty.
Warning: this will delete all aliases associated with the bucket!
#### CleanupIncompleteUploads `POST /v2/CleanupIncompleteUploads`
Cleanup all incomplete uploads in a bucket that are older than a specified number
of seconds.
Request body format:
```json
{
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
"olderThanSecs": 3600
}
```
Response format
```json
{
"uploadsDeleted": 12
}
```
### Operations on permissions for keys on buckets
#### BucketAllowKey `POST /v1/bucket/allow`
#### AllowBucketKey `POST /v2/AllowBucketKey`
Allows a key to do read/write/owner operations on a bucket.
@@ -710,7 +726,7 @@ Request body format:
Flags in `permissions` which have the value `true` will be activated.
Other flags will remain unchanged.
#### BucketDenyKey `POST /v1/bucket/deny`
#### DenyBucketKey `POST /v2/DenyBucketKey`
Denies a key from doing read/write/owner operations on a bucket.
@@ -734,19 +750,35 @@ Other flags will remain unchanged.
### Operations on bucket aliases
#### GlobalAliasBucket `PUT /v1/bucket/alias/global?id=<bucket id>&alias=<global alias>`
#### AddBucketAlias `POST /v2/AddBucketAlias`
Empty body. Creates a global alias for a bucket.
Creates an alias for a bucket in the namespace of a specific access key.
To create a global alias, specify the `globalAlias` field.
To create a local alias, specify the `localAlias` and `accessKeyId` fields.
#### GlobalUnaliasBucket `DELETE /v1/bucket/alias/global?id=<bucket id>&alias=<global alias>`
Request body format:
Removes a global alias for a bucket.
```json
{
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
"globalAlias": "my-bucket"
}
```
#### LocalAliasBucket `PUT /v1/bucket/alias/local?id=<bucket id>&accessKeyId=<access key ID>&alias=<local alias>`
or:
Empty body. Creates a local alias for a bucket in the namespace of a specific access key.
```json
{
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
"accessKeyId": "GK31c2f218a2e44f485b94239e",
"localAlias": "my-bucket"
}
```
#### LocalUnaliasBucket `DELETE /v1/bucket/alias/local?id=<bucket id>&accessKeyId<access key ID>&alias=<local alias>`
#### RemoveBucketAlias `POST /v2/RemoveBucketAlias`
Removes a local alias for a bucket in the namespace of a specific access key.
Removes an alias for a bucket in the namespace of a specific access key.
To remove a global alias, specify the `globalAlias` field.
To remove a local alias, specify the `localAlias` and `accessKeyId` fields.
Request body format: same as AddBucketAlias.
+2 -2
View File
@@ -146,7 +146,7 @@ in a bucket, as the partition key becomes the sort key in the index.
How indexing works:
- Each node keeps a local count of how many items it stores for each partition,
in a local Sled tree that is updated atomically when an item is modified.
in a local database tree that is updated atomically when an item is modified.
- These local counters are asynchronously stored in the index table which is
a regular Garage table spread in the network. Counters are stored as LWW values,
so basically the final table will have the following structure:
@@ -562,7 +562,7 @@ token>", v: ["<value1>", ...] }`, with the following fields:
- in case of concurrent update and deletion, a `null` is added to the list of concurrent values
- if the `tombstones` query parameter is set to `true`, tombstones are returned
for items that have been deleted (this can be usefull for inserting after an
for items that have been deleted (this can be useful for inserting after an
item that has been deleted, so that the insert is not considered
concurrent with the delete). Tombstones are returned as tuples in the
same format with only `null` values
Generated
+29 -89
View File
@@ -1,38 +1,27 @@
{
"nodes": {
"cargo2nix": {
"inputs": {
"flake-compat": [
"flake-compat"
],
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"crane": {
"locked": {
"lastModified": 1666087781,
"narHash": "sha256-trKVdjMZ8mNkGfLcY5LsJJGtdV3xJDZnMVrkFjErlcs=",
"owner": "Alexis211",
"repo": "cargo2nix",
"rev": "a7a61179b66054904ef6a195d8da736eaaa06c36",
"lastModified": 1737689766,
"narHash": "sha256-ivVXYaYlShxYoKfSo5+y5930qMKKJ8CLcAoIBPQfJ6s=",
"owner": "ipetkov",
"repo": "crane",
"rev": "6fe74265bbb6d016d663b1091f015e2976c4a527",
"type": "github"
},
"original": {
"owner": "Alexis211",
"repo": "cargo2nix",
"rev": "a7a61179b66054904ef6a195d8da736eaaa06c36",
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-compat": {
"locked": {
"lastModified": 1688025799,
"narHash": "sha256-ktpB4dRtnksm9F5WawoIkEneh1nrEvuxb5lJFt1iOyw=",
"lastModified": 1717312683,
"narHash": "sha256-FrlieJH50AuvagamEvWMIE6D2OAnERuDboFDYAED/dE=",
"owner": "nix-community",
"repo": "flake-compat",
"rev": "8bf105319d44f6b9f0d764efa4fdef9f1cc9ba1c",
"rev": "38fd3954cf65ce6faf3d0d45cd26059e059f07ea",
"type": "github"
},
"original": {
@@ -46,29 +35,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@@ -79,63 +50,47 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1682109806,
"narHash": "sha256-d9g7RKNShMLboTWwukM+RObDWWpHKaqTYXB48clBWXI=",
"lastModified": 1747825515,
"narHash": "sha256-BWpMQymVI73QoKZdcVCxUCCK3GNvr/xa2Dc4DM1o2BE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2362848adf8def2866fabbffc50462e929d7fffb",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1707091808,
"narHash": "sha256-LahKBAfGbY836gtpVNnWwBTIzN7yf/uYM/S0g393r0Y=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9f2ee8c91ac42da3ae6c6a1d21555f283458247e",
"rev": "cd2812de55cf87df88a9e09bf3be1ce63d50c1a6",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9f2ee8c91ac42da3ae6c6a1d21555f283458247e",
"rev": "cd2812de55cf87df88a9e09bf3be1ce63d50c1a6",
"type": "github"
}
},
"root": {
"inputs": {
"cargo2nix": "cargo2nix",
"crane": "crane",
"flake-compat": "flake-compat",
"flake-utils": [
"cargo2nix",
"flake-utils"
],
"nixpkgs": "nixpkgs_2"
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1707271822,
"narHash": "sha256-/DZsoPH5GBzOpVEGz5PgJ7vh8Q6TcrJq5u8FcBjqAfI=",
"lastModified": 1738549608,
"narHash": "sha256-GdyT9QEUSx5k/n8kILuNy83vxxdyUfJ8jL5mMpQZWfw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "7a94fe7690d2bdfe1aab475382a505e14dc114a6",
"rev": "35c6f8c4352f995ecd53896200769f80a3e8f22d",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "35c6f8c4352f995ecd53896200769f80a3e8f22d",
"type": "github"
}
},
@@ -153,21 +108,6 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+57 -61
View File
@@ -2,88 +2,84 @@
description =
"Garage, an S3-compatible distributed object store for self-hosted deployments";
# Nixpkgs 23.11 as of 2024-02-07, has rustc v1.73
# Nixpkgs 25.05 as of 2025-05-22
inputs.nixpkgs.url =
"github:NixOS/nixpkgs/9f2ee8c91ac42da3ae6c6a1d21555f283458247e";
"github:NixOS/nixpkgs/cd2812de55cf87df88a9e09bf3be1ce63d50c1a6";
# Rust overlay as of 2025-02-03
inputs.rust-overlay.url =
"github:oxalica/rust-overlay/35c6f8c4352f995ecd53896200769f80a3e8f22d";
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
inputs.crane.url = "github:ipetkov/crane";
inputs.flake-compat.url = "github:nix-community/flake-compat";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.cargo2nix = {
# As of 2022-10-18: two small patches over unstable branch, one for clippy and one to fix feature detection
url = "github:Alexis211/cargo2nix/a7a61179b66054904ef6a195d8da736eaaa06c36";
# As of 2023-04-25:
# - my two patches were merged into unstable (one for clippy and one to "fix" feature detection)
# - rustc v1.66
# url = "github:cargo2nix/cargo2nix/8fb57a670f7993bfc24099c33eb9c5abb51f29a2";
# Rust overlay as of 2024-02-07
inputs.rust-overlay.url =
"github:oxalica/rust-overlay/7a94fe7690d2bdfe1aab475382a505e14dc114a6";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-compat.follows = "flake-compat";
};
inputs.flake-utils.follows = "cargo2nix/flake-utils";
outputs = { self, nixpkgs, cargo2nix, flake-utils, ... }:
outputs = { self, nixpkgs, flake-utils, crane, rust-overlay, ... }:
let
git_version = self.lastModifiedDate;
compile = import ./nix/compile.nix;
in
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
packageFor = target: release: (compile {
inherit system target nixpkgs crane rust-overlay release;
}).garage;
testWith = extraTestEnv: (compile {
inherit system nixpkgs crane rust-overlay extraTestEnv;
release = false;
}).garage-test;
in
{
packages =
let
packageFor = target: (compile {
inherit system git_version target;
pkgsSrc = nixpkgs;
cargo2nixOverlay = cargo2nix.overlays.default;
release = true;
}).workspace.garage { compileMode = "build"; };
in
{
# default = native release build
default = packageFor null;
# other = cross-compiled, statically-linked builds
amd64 = packageFor "x86_64-unknown-linux-musl";
i386 = packageFor "i686-unknown-linux-musl";
arm64 = packageFor "aarch64-unknown-linux-musl";
arm = packageFor "armv6l-unknown-linux-musl";
packages = {
# default = native release build
default = packageFor null true;
# <arch> = cross-compiled, statically-linked release builds
amd64 = packageFor "x86_64-unknown-linux-musl" true;
i386 = packageFor "i686-unknown-linux-musl" true;
arm64 = packageFor "aarch64-unknown-linux-musl" true;
arm = packageFor "armv6l-unknown-linux-musl" true;
# dev = native dev build
dev = packageFor null false;
# test = cargo test
tests = testWith {};
tests-lmdb = testWith {
GARAGE_TEST_INTEGRATION_DB_ENGINE = "lmdb";
};
tests-sqlite = testWith {
GARAGE_TEST_INTEGRATION_DB_ENGINE = "sqlite";
};
};
# ---- developpment shell, for making native builds only ----
devShells =
let
shellWithPackages = (packages: (compile {
inherit system git_version;
pkgsSrc = nixpkgs;
cargo2nixOverlay = cargo2nix.overlays.default;
}).workspaceShell { inherit packages; });
targets = compile {
inherit system nixpkgs crane rust-overlay;
};
in
{
default = shellWithPackages
(with pkgs; [
rustfmt
clang
mold
]);
default = targets.devShell;
# import the full shell using `nix develop .#full`
full = shellWithPackages (with pkgs; [
rustfmt
clang
mold
# ---- extra packages for dev tasks ----
cargo-audit
cargo-outdated
cargo-machete
nixpkgs-fmt
]);
full = pkgs.mkShell {
buildInputs = with pkgs; [
targets.toolchain
protobuf
clang
mold
# ---- extra packages for dev tasks ----
rust-analyzer
cargo-audit
cargo-outdated
cargo-machete
nixpkgs-fmt
];
};
};
});
}
+1 -1
View File
@@ -2,7 +2,7 @@
with import ./common.nix;
let
pkgs = import pkgsSrc { };
pkgs = import nixpkgs { };
lib = pkgs.lib;
/* Converts a key list and a value list to a set
+5 -5
View File
@@ -10,9 +10,9 @@ let
flake = (import flake-compat { system = builtins.currentSystem; src = ../.; });
in
rec {
pkgsSrc = flake.defaultNix.inputs.nixpkgs;
cargo2nix = flake.defaultNix.inputs.cargo2nix;
cargo2nixOverlay = cargo2nix.overlays.default;
devShells = builtins.getAttr builtins.currentSystem flake.defaultNix.devShells;
{
flake = flake.defaultNix;
nixpkgs = flake.defaultNix.inputs.nixpkgs;
devShells = flake.defaultNix.devShells.${builtins.currentSystem};
}
+146 -172
View File
@@ -1,164 +1,64 @@
{ system, target ? null, pkgsSrc, cargo2nixOverlay, compiler ? "rustc"
, release ? false, git_version ? null, features ? null, }:
{
/* build inputs */
nixpkgs,
crane,
rust-overlay,
/* parameters */
system,
git_version ? null,
target ? null,
release ? false,
features ? null,
extraTestEnv ? {}
}:
let
log = v: builtins.trace v v;
# NixOS and Rust/Cargo triples do not match for ARM, fix it here.
rustTarget = if target == "armv6l-unknown-linux-musleabihf" then
"arm-unknown-linux-musleabihf"
else
target;
rustTargetEnvMap = {
"x86_64-unknown-linux-musl" = "X86_64_UNKNOWN_LINUX_MUSL";
"aarch64-unknown-linux-musl" = "AARCH64_UNKNOWN_LINUX_MUSL";
"i686-unknown-linux-musl" = "I686_UNKNOWN_LINUX_MUSL";
"arm-unknown-linux-musleabihf" = "ARM_UNKNOWN_LINUX_MUSLEABIHF";
};
pkgsNative = import nixpkgs {
inherit system;
overlays = [ (import rust-overlay) ];
};
pkgs = if target != null then
import pkgsSrc {
import nixpkgs {
inherit system;
crossSystem = {
config = target;
isStatic = true;
};
overlays = [ cargo2nixOverlay ];
overlays = [ (import rust-overlay) ];
}
else
import pkgsSrc {
inherit system;
overlays = [ cargo2nixOverlay ];
};
pkgsNative;
toolchainOptions = {
rustVersion = "1.73.0";
extraRustComponents = [ "clippy" ];
};
inherit (pkgs) lib stdenv;
buildEnv = (drv:
{
rustc = drv.setBuildEnv;
clippy = ''
${drv.setBuildEnv or ""}
echo
echo --- BUILDING WITH CLIPPY ---
echo
export NIX_RUST_BUILD_FLAGS="''${NIX_RUST_BUILD_FLAGS} --deny warnings"
export RUSTC="''${CLIPPY_DRIVER}"
'';
}.${compiler});
/* Cargo2nix provides many overrides by default, you can take inspiration from them:
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/overrides.nix
You can have a complete list of the available options by looking at the overriden object, mkcrate:
https://github.com/cargo2nix/cargo2nix/blob/master/overlay/mkcrate.nix
*/
packageOverrides = pkgs:
pkgs.rustBuilder.overrides.all ++ [
/* [1] We add some logic to compile our crates with clippy, it provides us many additional lints
[2] We need to alter Nix hardening to make static binaries: PIE,
Position Independent Executables seems to be supported only on amd64. Having
this flag set either 1. make our executables crash or 2. compile as dynamic on some platforms.
Here, we deactivate it. Later (find `codegenOpts`), we reactivate it for supported targets
(only amd64 curently) through the `-static-pie` flag.
PIE is a feature used by ASLR, which helps mitigate security issues.
Learn more about Nix Hardening at: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh
[3] We want to inject the git version while keeping the build deterministic.
As we do not want to consider the .git folder as part of the input source,
we ask the user (the CI often) to pass the value to Nix.
[4] We don't want libsodium-sys and zstd-sys to try to use pkgconfig to build against a system library.
However the features to do so get activated for some reason (due to a bug in cargo2nix?),
so disable them manually here.
*/
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage";
overrideAttrs = drv:
(if git_version != null then {
# [3]
preConfigure = ''
${drv.preConfigure or ""}
export GIT_VERSION="${git_version}"
'';
} else
{ }) // {
# [1]
setBuildEnv = (buildEnv drv);
# [2]
hardeningDisable = [ "pie" ];
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_rpc";
overrideAttrs = drv: { # [1]
setBuildEnv = (buildEnv drv);
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_db";
overrideAttrs = drv: { # [1]
setBuildEnv = (buildEnv drv);
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_util";
overrideAttrs = drv: { # [1]
setBuildEnv = (buildEnv drv);
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_table";
overrideAttrs = drv: { # [1]
setBuildEnv = (buildEnv drv);
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_block";
overrideAttrs = drv: { # [1]
setBuildEnv = (buildEnv drv);
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_model";
overrideAttrs = drv: { # [1]
setBuildEnv = (buildEnv drv);
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_api";
overrideAttrs = drv: { # [1]
setBuildEnv = (buildEnv drv);
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "garage_web";
overrideAttrs = drv: { # [1]
setBuildEnv = (buildEnv drv);
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "k2v-client";
overrideAttrs = drv: { # [1]
setBuildEnv = (buildEnv drv);
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "libsodium-sys";
overrideArgs = old: {
features = [ ]; # [4]
};
})
(pkgs.rustBuilder.rustLib.makeOverride {
name = "zstd-sys";
overrideArgs = old: {
features = [ ]; # [4]
};
})
toolchainFn = (p: p.rust-bin.stable."1.82.0".default.override {
targets = lib.optionals (target != null) [ rustTarget ];
extensions = [
"rust-src"
"rustfmt"
];
});
craneLib = (crane.mkLib pkgs).overrideToolchain toolchainFn;
src = craneLib.cleanCargoSource ../.;
/* We ship some parts of the code disabled by default by putting them behind a flag.
It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.).
@@ -168,16 +68,15 @@ let
rootFeatures = if features != null then
features
else
([ "garage/bundled-libs" "garage/sled" "garage/lmdb" "garage/sqlite" "garage/k2v" ] ++ (if release then [
"garage/consul-discovery"
"garage/kubernetes-discovery"
"garage/metrics"
"garage/telemetry-otlp"
"garage/syslog"
] else
[ ]));
([ "bundled-libs" "lmdb" "sqlite" "k2v" ] ++ (lib.optionals release [
"consul-discovery"
"kubernetes-discovery"
"metrics"
"telemetry-otlp"
"syslog"
]));
packageFun = import ../Cargo.nix;
featuresStr = lib.concatStringsSep "," rootFeatures;
/* We compile fully static binaries with musl to simplify deployment on most systems.
When possible, we reactivate PIE hardening (see above).
@@ -188,12 +87,9 @@ let
For more information on static builds, please refer to Rust's RFC 1721.
https://rust-lang.github.io/rfcs/1721-crt-static.html#specifying-dynamicstatic-c-runtime-linkage
*/
codegenOpts = {
"armv6l-unknown-linux-musleabihf" = [
"target-feature=+crt-static"
"link-arg=-static"
]; # compile as dynamic with static-pie
codegenOptsMap = {
"x86_64-unknown-linux-musl" =
[ "target-feature=+crt-static" "link-arg=-static-pie" ];
"aarch64-unknown-linux-musl" = [
"target-feature=+crt-static"
"link-arg=-static"
@@ -202,17 +98,95 @@ let
"target-feature=+crt-static"
"link-arg=-static"
]; # segfault with static-pie
"x86_64-unknown-linux-musl" =
[ "target-feature=+crt-static" "link-arg=-static-pie" ];
"armv6l-unknown-linux-musleabihf" = [
"target-feature=+crt-static"
"link-arg=-static"
]; # compile as dynamic with static-pie
};
# NixOS and Rust/Cargo triples do not match for ARM, fix it here.
rustTarget = if target == "armv6l-unknown-linux-musleabihf" then
"arm-unknown-linux-musleabihf"
else
target;
codegenOpts = if target != null then codegenOptsMap.${target} else [
"link-arg=-fuse-ld=mold"
];
in pkgs.rustBuilder.makePackageSet ({
inherit release packageFun packageOverrides codegenOpts rootFeatures;
target = rustTarget;
} // toolchainOptions)
commonArgs =
{
inherit src;
pname = "garage";
version = "dev";
strictDeps = true;
cargoExtraArgs = "--locked --features ${featuresStr}";
cargoTestExtraArgs = "--workspace";
nativeBuildInputs = [
pkgsNative.protobuf
pkgs.stdenv.cc
] ++ lib.optionals (target == null) [
pkgs.clang
pkgs.mold
];
CARGO_PROFILE = if release then "release" else "dev";
CARGO_BUILD_RUSTFLAGS =
lib.concatStringsSep
" "
(builtins.map (flag: "-C ${flag}") codegenOpts);
}
//
(if rustTarget != null then {
CARGO_BUILD_TARGET = rustTarget;
"CARGO_TARGET_${rustTargetEnvMap.${rustTarget}}_LINKER" = "${stdenv.cc.targetPrefix}cc";
HOST_CC = "${stdenv.cc.nativePrefix}cc";
TARGET_CC = "${stdenv.cc.targetPrefix}cc";
} else {
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = "clang";
});
in rec {
toolchain = toolchainFn pkgs;
devShell = pkgs.mkShell {
buildInputs = [
toolchain
] ++ (with pkgs; [
protobuf
clang
mold
]);
};
# ---- building garage ----
garage-deps = craneLib.buildDepsOnly commonArgs;
garage = craneLib.buildPackage (commonArgs // {
cargoArtifacts = garage-deps;
doCheck = false;
} //
(if git_version != null then {
version = git_version;
GIT_VERSION = git_version;
} else {}));
# ---- testing garage ----
garage-test-bin = craneLib.cargoBuild (commonArgs // {
cargoArtifacts = garage-deps;
pname = "garage-tests";
CARGO_PROFILE = "test";
cargoExtraArgs = "${commonArgs.cargoExtraArgs} --tests --workspace";
doCheck = false;
});
garage-test = craneLib.cargoTest (commonArgs // {
cargoArtifacts = garage-test-bin;
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [
pkgs.cacert
];
} // extraTestEnv);
}
+10 -4
View File
@@ -17,13 +17,19 @@ else
fi
$GARAGE_BIN -c /tmp/config.1.toml bucket create eprouvette
if [ "$GARAGE_08" = "1" ]; then
if [ "$GARAGE_OLDVER" = "v08" ]; then
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml key new --name opérateur)
else
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
elif [ "$GARAGE_OLDVER" = "v1" ]; then
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml key create opérateur)
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
else
KEY_INFO=$($GARAGE_BIN -c /tmp/config.1.toml json-api CreateKey '{"name":"opérateur"}')
ACCESS_KEY=`echo $KEY_INFO|jq -r .accessKeyId`
SECRET_KEY=`echo $KEY_INFO|jq -r .secretAccessKey`
fi
ACCESS_KEY=`echo $KEY_INFO|grep -Po 'GK[a-f0-9]+'`
SECRET_KEY=`echo $KEY_INFO|grep -Po 'Secret key: [a-f0-9]+'|grep -Po '[a-f0-9]+$'`
$GARAGE_BIN -c /tmp/config.1.toml bucket allow eprouvette --read --write --owner --key $ACCESS_KEY
echo "$ACCESS_KEY $SECRET_KEY" > /tmp/garage.s3
+8 -2
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_common=debug,garage_api_s3=debug
MAIN_LABEL="\e[${FANCYCOLORS[0]}[main]\e[49m"
if [ -z "$GARAGE_BIN" ]; then
@@ -30,6 +30,12 @@ for count in $(seq 1 3); do
CONF_PATH="/tmp/config.$count.toml"
LABEL="\e[${FANCYCOLORS[$count]}[$count]\e[49m"
if [ "$GARAGE_OLDVER" == "v08" ]; then
REPLICATION_MODE="replication_mode = \"3\""
else
REPLICATION_MODE="replication_factor = 3"
fi
cat > $CONF_PATH <<EOF
block_size = 1048576 # objects are split in blocks of maximum this number of bytes
metadata_dir = "/tmp/garage-meta-$count"
@@ -38,7 +44,7 @@ data_dir = "/tmp/garage-data-$count"
rpc_bind_addr = "0.0.0.0:$((3900+$count))" # the port other Garage nodes will use to talk to this node
rpc_public_addr = "127.0.0.1:$((3900+$count))"
bootstrap_peers = []
replication_mode = "3"
$REPLICATION_MODE
rpc_secret = "$NETWORK_SECRET"
[s3_api]
+1 -1
View File
@@ -29,7 +29,7 @@ until $GARAGE_BIN -c /tmp/config.1.toml status 2>&1|grep -q HEALTHY ; do
sleep 1
done
if [ "$GARAGE_08" = "1" ]; then
if [ "$GARAGE_OLDVER" = "v08" ]; then
$GARAGE_BIN -c /tmp/config.1.toml status \
| grep 'NO ROLE' \
| grep -Po '^[0-9a-f]+' \
+1 -1
View File
@@ -1,3 +1,3 @@
# Garage helm3 chart
Documentation is located [here](/doc/book/cookbook/kubernetes.md).
Documentation is located [here](https://garagehq.deuxfleurs.fr/documentation/cookbook/kubernetes/).
+12 -18
View File
@@ -1,24 +1,18 @@
apiVersion: v2
name: garage
description: S3-compatible object store for small self-hosted geo-distributed deployments
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
version: 0.8.0
appVersion: "v2.0.0"
home: https://garagehq.deuxfleurs.fr/
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.4.2
keywords:
- geo-distributed
- read-after-write-consistency
- s3-compatible
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v0.9.4"
sources:
- https://git.deuxfleurs.fr/Deuxfleurs/garage.git
maintainers: []
+96
View File
@@ -0,0 +1,96 @@
# garage
![Version: 0.8.0](https://img.shields.io/badge/Version-0.8.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2.0.0](https://img.shields.io/badge/AppVersion-v2.0.0-informational?style=flat-square)
S3-compatible object store for small self-hosted geo-distributed deployments
**Homepage:** <https://garagehq.deuxfleurs.fr/>
## Source Code
* <https://git.deuxfleurs.fr/Deuxfleurs/garage.git>
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | |
| deployment.kind | string | `"StatefulSet"` | Switchable to DaemonSet |
| deployment.podManagementPolicy | string | `"OrderedReady"` | If using statefulset, allow Parallel or OrderedReady (default) |
| deployment.replicaCount | int | `3` | Number of StatefulSet replicas/garage nodes to start |
| environment | object | `{}` | |
| extraVolumeMounts | object | `{}` | |
| extraVolumes | object | `{}` | |
| fullnameOverride | string | `""` | |
| garage.blockSize | string | `"1048576"` | Defaults is 1MB An increase can result in better performance in certain scenarios https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block_size |
| garage.bootstrapPeers | list | `[]` | This is not required if you use the integrated kubernetes discovery |
| garage.compressionLevel | string | `"1"` | zstd compression level of stored blocks https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression_level |
| garage.dbEngine | string | `"lmdb"` | Can be changed for better performance on certain systems https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine |
| garage.existingConfigMap | string | `""` | if not empty string, allow using an existing ConfigMap for the garage.toml, if set, ignores garage.toml |
| garage.garageTomlString | string | `""` | String Template for the garage configuration if set, ignores above values. Values can be templated, see https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/ |
| garage.kubernetesSkipCrd | bool | `false` | Set to true if you want to use k8s discovery but install the CRDs manually outside of the helm chart, for example if you operate at namespace level without cluster ressources |
| garage.replicationFactor | string | `"3"` | Default to 3 replicas, see the replication_factor section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor |
| garage.consistencyMode | string | `"consistent"` | Default to read-after-write consistency, see the consistency_mode section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode |
| garage.metadataAutoSnapshotInterval | string | `""` | If this value is set, Garage will automatically take a snapshot of the metadata DB file at a regular interval and save it in the metadata directory. https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval |
| garage.rpcBindAddr | string | `"[::]:3901"` | |
| garage.rpcSecret | string | `""` | If not given, a random secret will be generated and stored in a Secret object |
| garage.s3.api.region | string | `"garage"` | |
| garage.s3.api.rootDomain | string | `".s3.garage.tld"` | |
| garage.s3.web.index | string | `"index.html"` | |
| garage.s3.web.rootDomain | string | `".web.garage.tld"` | |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"dxflrs/amd64_garage"` | default to amd64 docker image |
| image.tag | string | `""` | set the image tag, please prefer using the chart version and not this to avoid compatibility issues |
| imagePullSecrets | list | `[]` | set if you need credentials to pull your custom image |
| ingress.s3.api.annotations | object | `{}` | Rely _either_ on the className or the annotation below but not both! If you want to use the className, set className: "nginx" and replace "nginx" by an Ingress controller name, examples [here](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers). |
| ingress.s3.api.enabled | bool | `false` | |
| ingress.s3.api.hosts[0] | object | `{"host":"s3.garage.tld","paths":[{"path":"/","pathType":"Prefix"}]}` | garage S3 API endpoint, to be used with awscli for example |
| ingress.s3.api.hosts[1] | object | `{"host":"*.s3.garage.tld","paths":[{"path":"/","pathType":"Prefix"}]}` | garage S3 API endpoint, DNS style bucket access |
| ingress.s3.api.labels | object | `{}` | |
| ingress.s3.api.tls | list | `[]` | |
| ingress.s3.web.annotations | object | `{}` | Rely _either_ on the className or the annotation below but not both! If you want to use the className, set className: "nginx" and replace "nginx" by an Ingress controller name, examples [here](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers). |
| ingress.s3.web.enabled | bool | `false` | |
| ingress.s3.web.hosts[0] | object | `{"host":"*.web.garage.tld","paths":[{"path":"/","pathType":"Prefix"}]}` | wildcard website access with bucket name prefix |
| ingress.s3.web.hosts[1] | object | `{"host":"mywebpage.example.com","paths":[{"path":"/","pathType":"Prefix"}]}` | specific bucket access with FQDN bucket |
| ingress.s3.web.labels | object | `{}` | |
| ingress.s3.web.tls | list | `[]` | |
| initImage.pullPolicy | string | `"IfNotPresent"` | |
| initImage.repository | string | `"busybox"` | |
| initImage.tag | string | `"stable"` | |
| livenessProbe | object | `{}` | Specifies a livenessProbe |
| monitoring.metrics.enabled | bool | `false` | If true, a service for monitoring is created with a prometheus.io/scrape annotation |
| monitoring.metrics.serviceMonitor.enabled | bool | `false` | If true, a ServiceMonitor CRD is created for a prometheus operator https://github.com/coreos/prometheus-operator |
| monitoring.metrics.serviceMonitor.interval | string | `"15s"` | |
| monitoring.metrics.serviceMonitor.labels | object | `{}` | |
| monitoring.metrics.serviceMonitor.path | string | `"/metrics"` | |
| monitoring.metrics.serviceMonitor.relabelings | list | `[]` | |
| monitoring.metrics.serviceMonitor.scheme | string | `"http"` | |
| monitoring.metrics.serviceMonitor.scrapeTimeout | string | `"10s"` | |
| monitoring.metrics.serviceMonitor.tlsConfig | object | `{}` | |
| monitoring.tracing.sink | string | `""` | specify a sink endpoint for OpenTelemetry Traces, eg. `http://localhost:4317` |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | |
| persistence.data.hostPath | string | `"/var/lib/garage/data"` | |
| persistence.data.size | string | `"100Mi"` | |
| persistence.enabled | bool | `true` | |
| persistence.meta.hostPath | string | `"/var/lib/garage/meta"` | |
| persistence.meta.size | string | `"100Mi"` | |
| podAnnotations | object | `{}` | additonal pod annotations |
| podSecurityContext.fsGroup | int | `1000` | |
| podSecurityContext.runAsGroup | int | `1000` | |
| podSecurityContext.runAsNonRoot | bool | `true` | |
| podSecurityContext.runAsUser | int | `1000` | |
| readinessProbe | object | `{}` | Specifies a readinessProbe |
| resources | object | `{}` | |
| securityContext.capabilities | object | `{"drop":["ALL"]}` | The default security context is heavily restricted, feel free to tune it to your requirements |
| securityContext.readOnlyRootFilesystem | bool | `true` | |
| service.s3.api.port | int | `3900` | |
| service.s3.web.port | int | `3902` | |
| service.type | string | `"ClusterIP"` | You can rely on any service to expose your cluster - ClusterIP (+ Ingress) - NodePort (+ Ingress) - LoadBalancer |
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
| tolerations | list | `[]` | |
----------------------------------------------
Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2)
+48 -1
View File
@@ -1,7 +1,54 @@
{{- if not .Values.garage.existingConfigMap }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "garage.fullname" . }}-config
data:
garage.toml: |-
{{- tpl (index (index .Values.garage) "garage.toml") $ | nindent 4 }}
{{- if .Values.garage.garageTomlString }}
{{- tpl (index (index .Values.garage) "garageTomlString") $ | nindent 4 }}
{{- else }}
metadata_dir = "/mnt/meta"
data_dir = "/mnt/data"
db_engine = "{{ .Values.garage.dbEngine }}"
block_size = {{ .Values.garage.blockSize }}
replication_factor = {{ .Values.garage.replicationFactor }}
consistency_mode = "{{ .Values.garage.consistencyMode }}"
compression_level = {{ .Values.garage.compressionLevel }}
{{- if .Values.garage.metadataAutoSnapshotInterval }}
metadata_auto_snapshot_interval = {{ .Values.garage.metadataAutoSnapshotInterval | quote }}
{{- end }}
rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}"
# rpc_secret will be populated by the init container from a k8s secret object
rpc_secret = "__RPC_SECRET_REPLACE__"
bootstrap_peers = {{ .Values.garage.bootstrapPeers }}
[kubernetes_discovery]
namespace = "{{ .Release.Namespace }}"
service_name = "{{ include "garage.fullname" . }}"
skip_crd = {{ .Values.garage.kubernetesSkipCrd }}
[s3_api]
s3_region = "{{ .Values.garage.s3.api.region }}"
api_bind_addr = "[::]:3900"
root_domain = "{{ .Values.garage.s3.api.rootDomain }}"
[s3_web]
bind_addr = "[::]:3902"
root_domain = "{{ .Values.garage.s3.web.rootDomain }}"
index = "{{ .Values.garage.s3.web.index }}"
[admin]
api_bind_addr = "[::]:3903"
{{- if .Values.monitoring.tracing.sink }}
trace_sink = "{{ .Values.monitoring.tracing.sink }}"
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,22 @@
{{- if eq .Values.deployment.kind "StatefulSet" -}}
apiVersion: v1
kind: Service
metadata:
name: {{ include "garage.fullname" . }}-headless
labels:
{{- include "garage.labels" . | nindent 4 }}
spec:
type: ClusterIP
clusterIP: None
ports:
- port: {{ .Values.service.s3.api.port }}
targetPort: 3900
protocol: TCP
name: s3-api
- port: {{ .Values.service.s3.web.port }}
targetPort: 3902
protocol: TCP
name: s3-web
selector:
{{- include "garage.selectorLabels" . | nindent 4 }}
{{- end }}
+20 -11
View File
@@ -10,11 +10,11 @@ spec:
{{- include "garage.selectorLabels" . | nindent 6 }}
{{- if eq .Values.deployment.kind "StatefulSet" }}
replicas: {{ .Values.deployment.replicaCount }}
serviceName: {{ include "garage.fullname" . }}
serviceName: {{ include "garage.fullname" . }}-headless
podManagementPolicy: {{ .Values.deployment.podManagementPolicy }}
{{- end }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
@@ -63,6 +63,10 @@ spec:
name: web-api
- containerPort: 3903
name: admin
{{- with .Values.environment }}
env:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: meta
mountPath: /mnt/meta
@@ -71,15 +75,17 @@ spec:
- name: etc
mountPath: /etc/garage.toml
subPath: garage.toml
# TODO
# livenessProbe:
# httpGet:
# path: /
# port: 3900
# readinessProbe:
# httpGet:
# path: /
# port: 3900
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
@@ -105,6 +111,9 @@ spec:
- name: data
emptyDir: {}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
+83 -80
View File
@@ -4,33 +4,38 @@
# Garage configuration. These values go to garage.toml
garage:
# Can be changed for better performance on certain systems
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db-engine-since-v0-8-0
dbEngine: "sled"
# -- Can be changed for better performance on certain systems
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine
dbEngine: "lmdb"
# Defaults is 1MB
# -- Defaults is 1MB
# An increase can result in better performance in certain scenarios
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block-size
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block_size
blockSize: "1048576"
# Tuning parameters for the sled DB engine
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#sled-cache-capacity
sledCacheCapacity: "134217728"
sledFlushEveryMs: "2000"
# -- Default to 3 replicas, see the replication_factor section at
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor
replicationFactor: "3"
# Default to 3 replicas, see the replication_mode section at
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication-mode
replicationMode: "3"
# -- By default, enable read-after-write consistency guarantees, see the consistency_mode section at
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode
consistencyMode: "consistent"
# zstd compression level of stored blocks
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression-level
# -- zstd compression level of stored blocks
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression_level
compressionLevel: "1"
# -- If this value is set, Garage will automatically take a snapshot of the metadata DB file at a regular interval and save it in the metadata directory.
# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval
metadataAutoSnapshotInterval: ""
rpcBindAddr: "[::]:3901"
# If not given, a random secret will be generated and stored in a Secret object
# -- If not given, a random secret will be generated and stored in a Secret object
rpcSecret: ""
# This is not required if you use the integrated kubernetes discovery
# -- This is not required if you use the integrated kubernetes discovery
bootstrapPeers: []
# -- Set to true if you want to use k8s discovery but install the CRDs manually outside
# of the helm chart, for example if you operate at namespace level without cluster ressources
kubernetesSkipCrd: false
s3:
api:
@@ -39,52 +44,16 @@ garage:
web:
rootDomain: ".web.garage.tld"
index: "index.html"
# Template for the garage configuration
# Values can be templated
# ref: https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/
garage.toml: |-
metadata_dir = "/mnt/meta"
data_dir = "/mnt/data"
db_engine = "{{ .Values.garage.dbEngine }}"
# -- if not empty string, allow using an existing ConfigMap for the garage.toml,
# if set, ignores garage.toml
existingConfigMap: ""
block_size = {{ .Values.garage.blockSize }}
{{- if eq .Values.garage.dbEngine "sled"}}
sled_cache_capacity = {{ .Values.garage.sledCacheCapacity }}
sled_flush_every_ms = {{ .Values.garage.sledFlushEveryMs }}
{{- end }}
replication_mode = "{{ .Values.garage.replicationMode }}"
compression_level = {{ .Values.garage.compressionLevel }}
rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}"
# rpc_secret will be populated by the init container from a k8s secret object
rpc_secret = "__RPC_SECRET_REPLACE__"
bootstrap_peers = {{ .Values.garage.bootstrapPeers }}
[kubernetes_discovery]
namespace = "{{ .Release.Namespace }}"
service_name = "{{ include "garage.fullname" . }}"
skip_crd = {{ .Values.garage.kubernetesSkipCrd }}
[s3_api]
s3_region = "{{ .Values.garage.s3.api.region }}"
api_bind_addr = "[::]:3900"
root_domain = "{{ .Values.garage.s3.api.rootDomain }}"
[s3_web]
bind_addr = "[::]:3902"
root_domain = "{{ .Values.garage.s3.web.rootDomain }}"
index = "{{ .Values.garage.s3.web.index }}"
[admin]
api_bind_addr = "[::]:3903"
{{- if .Values.monitoring.tracing.sink }}
trace_sink = "{{ .Values.monitoring.tracing.sink }}"
{{- end }}
# -- String Template for the garage configuration
# if set, ignores above values.
# Values can be templated,
# see https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/
garageTomlString: ""
# Data persistence
persistence:
@@ -102,14 +71,18 @@ persistence:
# Deployment configuration
deployment:
# Switchable to DaemonSet
# -- Switchable to DaemonSet
kind: StatefulSet
# Number of StatefulSet replicas/garage nodes to start
# -- Number of StatefulSet replicas/garage nodes to start
replicaCount: 3
# -- If using statefulset, allow Parallel or OrderedReady (default)
podManagementPolicy: OrderedReady
image:
# -- default to amd64 docker image
repository: dxflrs/amd64_garage
# please prefer using the chart version and not this tag
# -- set the image tag, please prefer using the chart version and not this
# to avoid compatibility issues
tag: ""
pullPolicy: IfNotPresent
@@ -118,19 +91,21 @@ initImage:
tag: stable
pullPolicy: IfNotPresent
# -- set if you need credentials to pull your custom image
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
# -- Specifies whether a service account should be created
create: true
# Annotations to add to the service account
# -- Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# -- The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# -- additonal pod annotations
podAnnotations: {}
podSecurityContext:
@@ -140,7 +115,7 @@ podSecurityContext:
runAsNonRoot: true
securityContext:
# The default security context is heavily restricted
# -- The default security context is heavily restricted,
# feel free to tune it to your requirements
capabilities:
drop:
@@ -148,7 +123,7 @@ securityContext:
readOnlyRootFilesystem: true
service:
# You can rely on any service to expose your cluster
# -- You can rely on any service to expose your cluster
# - ClusterIP (+ Ingress)
# - NodePort (+ Ingress)
# - LoadBalancer
@@ -164,20 +139,23 @@ ingress:
s3:
api:
enabled: false
# Rely either on the className or the annotation below but not both
# replace "nginx" by an Ingress controller
# you can find examples here https://kubernetes.io/docs/concepts/services-networking/ingress-controllers
# -- Rely _either_ on the className or the annotation below but not both!
# If you want to use the className, set
# className: "nginx"
# and replace "nginx" by an Ingress controller name,
# examples [here](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers).
annotations: {}
# kubernetes.io/ingress.class: "nginx"
# kubernetes.io/tls-acme: "true"
labels: {}
hosts:
- host: "s3.garage.tld" # garage S3 API endpoint
# -- garage S3 API endpoint, to be used with awscli for example
- host: "s3.garage.tld"
paths:
- path: /
pathType: Prefix
- host: "*.s3.garage.tld" # garage S3 API endpoint, DNS style bucket access
# -- garage S3 API endpoint, DNS style bucket access
- host: "*.s3.garage.tld"
paths:
- path: /
pathType: Prefix
@@ -187,20 +165,23 @@ ingress:
# - kubernetes.docker.internal
web:
enabled: false
# Rely either on the className or the annotation below but not both
# replace "nginx" by an Ingress controller
# you can find examples here https://kubernetes.io/docs/concepts/services-networking/ingress-controllers
# -- Rely _either_ on the className or the annotation below but not both!
# If you want to use the className, set
# className: "nginx"
# and replace "nginx" by an Ingress controller name,
# examples [here](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers).
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
labels: {}
hosts:
- host: "*.web.garage.tld" # wildcard website access with bucket name prefix
# -- wildcard website access with bucket name prefix
- host: "*.web.garage.tld"
paths:
- path: /
pathType: Prefix
- host: "mywebpage.example.com" # specific bucket access with FQDN bucket
# -- specific bucket access with FQDN bucket
- host: "mywebpage.example.com"
paths:
- path: /
pathType: Prefix
@@ -218,18 +199,39 @@ resources: {}
# cpu: 100m
# memory: 512Mi
# -- Specifies a livenessProbe
livenessProbe: {}
#httpGet:
# path: /health
# port: 3903
#initialDelaySeconds: 5
#periodSeconds: 30
# -- Specifies a readinessProbe
readinessProbe: {}
#httpGet:
# path: /health
# port: 3903
#initialDelaySeconds: 5
#periodSeconds: 30
nodeSelector: {}
tolerations: []
affinity: {}
environment: {}
extraVolumes: {}
extraVolumeMounts: {}
monitoring:
metrics:
# If true, a service for monitoring is created with a prometheus.io/scrape annotation
# -- If true, a service for monitoring is created with a prometheus.io/scrape annotation
enabled: false
serviceMonitor:
# If true, a ServiceMonitor CRD is created for a prometheus operator
# -- If true, a ServiceMonitor CRD is created for a prometheus operator
# https://github.com/coreos/prometheus-operator
enabled: false
path: /metrics
@@ -241,4 +243,5 @@ monitoring:
scrapeTimeout: 10s
relabelings: []
tracing:
# -- specify a sink endpoint for OpenTelemetry Traces, eg. `http://localhost:4317`
sink: ""
@@ -43,7 +43,7 @@
"rpc_bind_addr = \"0.0.0.0:3901\"\n"
"rpc_public_addr = \"" node ":3901\"\n"
"db_engine = \"lmdb\"\n"
"replication_mode = \"3\"\n"
"replication_factor = 3\n"
"data_dir = \"" data-dir "\"\n"
"metadata_dir = \"" meta-dir "\"\n"
"[s3_api]\n"
+1 -1
View File
@@ -8,7 +8,7 @@ data:
metadata_dir = "/tmp/meta"
data_dir = "/tmp/data"
replication_mode = "3"
replication_factor = 3
rpc_bind_addr = "[::]:3901"
rpc_secret = "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec"
@@ -0,0 +1,43 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: garagenodes.deuxfleurs.fr
spec:
conversion:
strategy: None
group: deuxfleurs.fr
names:
kind: GarageNode
listKind: GarageNodeList
plural: garagenodes
singular: garagenode
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
description: Auto-generated derived type for Node via `CustomResource`
properties:
spec:
properties:
address:
format: ip
type: string
hostname:
type: string
port:
format: uint16
minimum: 0
type: integer
required:
- address
- hostname
- port
type: object
required:
- spec
title: GarageNode
type: object
served: true
storage: true
subresources: {}
+5
View File
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- garagenodes.deuxfleurs.fr.yaml
+6 -1
View File
@@ -7,7 +7,12 @@ if [ "$#" -ne 1 ]; then
exit 2
fi
if file $1 | grep 'dynamically linked' 2>&1; then
if [ ! -x "$1" ]; then
echo "[fail] $1 does not exist or is not an executable"
exit 1
fi
if file "$1" | grep 'dynamically linked' 2>&1; then
echo "[fail] $1 is dynamic"
exit 1
fi
+30
View File
@@ -82,6 +82,19 @@ if [ -z "$SKIP_AWS" ]; then
exit 1
fi
aws s3api delete-object --bucket eprouvette --key upload
echo "🛠️ Test SSE-C with awscli (aws s3)"
SSEC_KEY="u8zCfnEyt5Imo/krN+sxA1DQXxLWtPJavU6T6gOVj1Y="
SSEC_KEY_MD5="jMGbs3GyZkYjJUP6q5jA7g=="
echo "$SSEC_KEY" | base64 -d > /tmp/garage.ssec-key
for idx in {1,2}.rnd; do
aws s3 cp --sse-c AES256 --sse-c-key fileb:///tmp/garage.ssec-key \
"/tmp/garage.$idx" "s3://eprouvette/garage.$idx.aws.sse-c"
aws s3 cp --sse-c AES256 --sse-c-key fileb:///tmp/garage.ssec-key \
"s3://eprouvette/garage.$idx.aws.sse-c" "/tmp/garage.$idx.dl.sse-c"
diff "/tmp/garage.$idx" "/tmp/garage.$idx.dl.sse-c"
aws s3api delete-object --bucket eprouvette --key "garage.$idx.aws.sse-c"
done
fi
# S3CMD
@@ -99,6 +112,23 @@ if [ -z "$SKIP_S3CMD" ]; then
done
fi
# BOTO3
if [ -z "$SKIP_BOTO3" ]; then
echo "🛠️ Testing with boto3 for STREAMING-UNSIGNED-PAYLOAD-TRAILER"
source ${SCRIPT_FOLDER}/dev-env-aws.sh
AWS_ENDPOINT_URL=https://localhost:4443 python <<EOF
import boto3
client = boto3.client('s3', verify=False)
print("Put&delete hello world object")
client.put_object(Body=b'hello world', Bucket='eprouvette', Key='test.s3.txt')
client.delete_object(Bucket='eprouvette', Key='test.s3.txt')
print("Put&delete big object")
client.upload_file("/tmp/garage.3.rnd", 'eprouvette', 'garage.3.rnd')
client.delete_object(Bucket='eprouvette', Key='garage.3.rnd')
print("OK!")
EOF
fi
# Minio Client
if [ -z "$SKIP_MC" ]; then
echo "🛠️ Testing with mc (minio client)"
+46 -4
View File
@@ -24,9 +24,17 @@ echo "============= insert data into old version cluster ================="
export GARAGE_BIN=/tmp/old_garage
if echo $OLD_VERSION | grep 'v0\.8\.'; then
echo "Detected Garage v0.8.x"
export GARAGE_08=1
export GARAGE_OLDVER=v08
elif (echo $OLD_VERSION | grep 'v0\.9\.') || (echo $OLD_VERSION | grep 'v1\.'); then
echo "Detected Garage v0.9.x / v1.x"
export GARAGE_OLDVER=v1
fi
if echo $OLD_VERSION | grep 'v1\.'; then
DO_SSEC_TEST=1
fi
SSEC_KEY="u8zCfnEyt5Imo/krN+sxA1DQXxLWtPJavU6T6gOVj1Y="
echo "⏳ Setup cluster using old version"
$GARAGE_BIN --version
${SCRIPT_FOLDER}/dev-clean.sh
@@ -37,7 +45,23 @@ ${SCRIPT_FOLDER}/dev-bucket.sh
echo "🛠️ Inserting data in old cluster"
source ${SCRIPT_FOLDER}/dev-env-rclone.sh
rclone copy "${SCRIPT_FOLDER}/../.git/" garage:eprouvette/test_dotgit --stats=1s --stats-log-level=NOTICE --stats-one-line
rclone copy "${SCRIPT_FOLDER}/../.git/" garage:eprouvette/test_dotgit \
--stats=1s --stats-log-level=NOTICE --stats-one-line
if [ "$DO_SSEC_TEST" = "1" ]; then
# upload small file (should be single part)
rclone copy "${SCRIPT_FOLDER}/test-upgrade.sh" garage:eprouvette/test-ssec \
--s3-sse-customer-algorithm AES256 \
--s3-sse-customer-key-base64 "$SSEC_KEY" \
--stats=1s --stats-log-level=NOTICE --stats-one-line
# do a multipart upload
dd if=/dev/urandom of=/tmp/randfile-for-upgrade bs=5M count=5
rclone copy "/tmp/randfile-for-upgrade" garage:eprouvette/test-ssec \
--s3-chunk-size 5M \
--s3-sse-customer-algorithm AES256 \
--s3-sse-customer-key-base64 "$SSEC_KEY" \
--stats=1s --stats-log-level=NOTICE --stats-one-line
fi
echo "🏁 Stopping old cluster"
killall -INT old_garage
@@ -47,7 +71,7 @@ killall -9 old_garage || true
echo "🏁 Removing old garage version"
rm -rv $GARAGE_BIN
export -n GARAGE_BIN
export -n GARAGE_08
export -n GARAGE_OLDVER
echo "================ read data from new cluster ==================="
@@ -60,7 +84,8 @@ ${SCRIPT_FOLDER}/dev-cluster.sh >> /tmp/garage.log 2>&1 &
sleep 3
echo "🛠️ Retrieving data from old cluster"
rclone copy garage:eprouvette/test_dotgit /tmp/test_dotgit --stats=1s --stats-log-level=NOTICE --stats-one-line --fast-list
rclone copy garage:eprouvette/test_dotgit /tmp/test_dotgit \
--stats=1s --stats-log-level=NOTICE --stats-one-line --fast-list
if ! diff <(find "${SCRIPT_FOLDER}/../.git" -type f | xargs md5sum | cut -d ' ' -f 1 | sort) <(find /tmp/test_dotgit -type f | xargs md5sum | cut -d ' ' -f 1 | sort); then
echo "TEST FAILURE: directories are different"
@@ -68,6 +93,23 @@ if ! diff <(find "${SCRIPT_FOLDER}/../.git" -type f | xargs md5sum | cut -d ' '
fi
rm -r /tmp/test_dotgit
if [ "$DO_SSEC_TEST" = "1" ]; then
rclone copy garage:eprouvette/test-ssec /tmp/test_ssec_out \
--s3-sse-customer-algorithm AES256 \
--s3-sse-customer-key-base64 "$SSEC_KEY" \
--stats=1s --stats-log-level=NOTICE --stats-one-line
if ! diff "/tmp/test_ssec_out/test-upgrade.sh" "${SCRIPT_FOLDER}/test-upgrade.sh"; then
echo "SSEC-FAILURE (small file)"
exit 1
fi
if ! diff "/tmp/test_ssec_out/randfile-for-upgrade" "/tmp/randfile-for-upgrade"; then
echo "SSEC-FAILURE (big file)"
exit 1
fi
rm -r /tmp/test_ssec_out
rm /tmp/randfile-for-upgrade
fi
echo "🏁 Teardown"
rm -rf /tmp/garage-{data,meta}-*
rm -rf /tmp/config.*.toml
+9 -3
View File
@@ -3,7 +3,7 @@
with import ./nix/common.nix;
let
pkgs = import pkgsSrc {
pkgs = import nixpkgs {
inherit system;
};
winscp = (import ./nix/winscp.nix) pkgs;
@@ -11,6 +11,7 @@ in
{
# --- Dev shell inherited from flake.nix ---
devShell = devShells.default;
devShellFull = devShells.full;
# --- Continuous integration shell ---
# The shell used for all CI jobs (along with devShell)
@@ -25,6 +26,8 @@ in
s3cmd
minio-client
rclone
(python312.withPackages (ps: [ ps.boto3 ]))
socat
psmisc
which
@@ -34,11 +37,12 @@ in
];
shellHook = ''
function to_s3 {
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED \
aws \
--endpoint-url https://garage.deuxfleurs.fr \
--region garage \
s3 cp \
./result-bin/bin/garage \
./result/bin/garage \
s3://garagehq.deuxfleurs.fr/_releases/''${CI_COMMIT_TAG:-$CI_COMMIT_SHA}/''${TARGET}/garage
}
@@ -90,6 +94,7 @@ in
nix-build nix/build_index.nix
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED \
aws \
--endpoint-url https://garage.deuxfleurs.fr \
--region garage \
@@ -97,6 +102,7 @@ in
result/share/_releases.json \
s3://garagehq.deuxfleurs.fr/
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED \
aws \
--endpoint-url https://garage.deuxfleurs.fr \
--region garage \
@@ -114,7 +120,7 @@ in
shellHook = ''
function refresh_cache {
pass show deuxfleurs/nix_priv_key > /tmp/nix-signing-key.sec
for attr in clippy.amd64 test.amd64 pkgs.{amd64,i386,arm,arm64}.release; do
for attr in pkgs.amd64.debug test.amd64 pkgs.{amd64,i386,arm,arm64}.release; do
echo "Updating cache for ''${attr}"
nix copy -j8 \
--to 's3://nix?endpoint=garage.deuxfleurs.fr&region=garage&secret-key=/tmp/nix-signing-key.sec' \
+50
View File
@@ -0,0 +1,50 @@
[package]
name = "garage_api_admin"
version = "2.0.0"
authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018"
license = "AGPL-3.0"
description = "Admin API server crate for the Garage object store"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
readme = "../../../README.md"
[lib]
path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
format_table.workspace = true
garage_model.workspace = true
garage_block.workspace = true
garage_table.workspace = true
garage_util.workspace = true
garage_rpc.workspace = true
garage_api_common.workspace = true
argon2.workspace = true
async-trait.workspace = true
bytesize.workspace = true
chrono.workspace = true
err-derive.workspace = true
hex.workspace = true
paste.workspace = true
tracing.workspace = true
futures.workspace = true
tokio.workspace = true
http.workspace = true
hyper = { workspace = true, default-features = false, features = ["server", "http1"] }
url.workspace = true
serde.workspace = true
serde_json.workspace = true
utoipa.workspace = true
opentelemetry.workspace = true
opentelemetry-prometheus = { workspace = true, optional = true }
prometheus = { workspace = true, optional = true }
[features]
metrics = [ "opentelemetry-prometheus", "prometheus" ]
k2v = [ "garage_model/k2v" ]
+235
View File
@@ -0,0 +1,235 @@
use std::sync::Arc;
use chrono::{DateTime, Utc};
use garage_table::*;
use garage_util::time::now_msec;
use garage_model::admin_token_table::*;
use garage_model::garage::Garage;
use crate::api::*;
use crate::error::*;
use crate::{Admin, RequestHandler};
impl RequestHandler for ListAdminTokensRequest {
type Response = ListAdminTokensResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<ListAdminTokensResponse, Error> {
let now = now_msec();
let mut res = garage
.admin_token_table
.get_range(
&EmptyKey,
None,
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
10000,
EnumerationOrder::Forward,
)
.await?
.iter()
.map(|t| admin_token_info_results(t, now))
.collect::<Vec<_>>();
if garage.config.admin.metrics_token.is_some() {
res.insert(
0,
GetAdminTokenInfoResponse {
id: None,
created: None,
name: "metrics_token (from daemon configuration)".into(),
expiration: None,
expired: false,
scope: vec!["Metrics".into()],
},
);
}
if garage.config.admin.admin_token.is_some() {
res.insert(
0,
GetAdminTokenInfoResponse {
id: None,
created: None,
name: "admin_token (from daemon configuration)".into(),
expiration: None,
expired: false,
scope: vec!["*".into()],
},
);
}
Ok(ListAdminTokensResponse(res))
}
}
impl RequestHandler for GetAdminTokenInfoRequest {
type Response = GetAdminTokenInfoResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<GetAdminTokenInfoResponse, Error> {
let token = match (self.id, self.search) {
(Some(id), None) => get_existing_admin_token(garage, &id).await?,
(None, Some(search)) => {
let candidates = garage
.admin_token_table
.get_range(
&EmptyKey,
None,
Some(KeyFilter::MatchesAndNotDeleted(search.to_string())),
10,
EnumerationOrder::Forward,
)
.await?
.into_iter()
.collect::<Vec<_>>();
if candidates.len() != 1 {
return Err(Error::bad_request(format!(
"{} matching admin tokens",
candidates.len()
)));
}
candidates.into_iter().next().unwrap()
}
_ => {
return Err(Error::bad_request(
"Either id or search must be provided (but not both)",
));
}
};
Ok(admin_token_info_results(&token, now_msec()))
}
}
impl RequestHandler for CreateAdminTokenRequest {
type Response = CreateAdminTokenResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<CreateAdminTokenResponse, Error> {
let (mut token, secret) = if self.0.name.is_some() {
AdminApiToken::new("")
} else {
AdminApiToken::new(&format!("token_{}", Utc::now().format("%Y%m%d_%H%M")))
};
apply_token_updates(&mut token, self.0)?;
garage.admin_token_table.insert(&token).await?;
Ok(CreateAdminTokenResponse {
secret_token: secret,
info: admin_token_info_results(&token, now_msec()),
})
}
}
impl RequestHandler for UpdateAdminTokenRequest {
type Response = UpdateAdminTokenResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<UpdateAdminTokenResponse, Error> {
let mut token = get_existing_admin_token(&garage, &self.id).await?;
apply_token_updates(&mut token, self.body)?;
garage.admin_token_table.insert(&token).await?;
Ok(UpdateAdminTokenResponse(admin_token_info_results(
&token,
now_msec(),
)))
}
}
impl RequestHandler for DeleteAdminTokenRequest {
type Response = DeleteAdminTokenResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<DeleteAdminTokenResponse, Error> {
let token = get_existing_admin_token(&garage, &self.id).await?;
garage
.admin_token_table
.insert(&AdminApiToken::delete(token.prefix))
.await?;
Ok(DeleteAdminTokenResponse)
}
}
// ---- helpers ----
fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInfoResponse {
let params = token.params().unwrap();
GetAdminTokenInfoResponse {
id: Some(token.prefix.clone()),
created: Some(
DateTime::from_timestamp_millis(params.created as i64)
.expect("invalid timestamp stored in db"),
),
name: params.name.get().to_string(),
expiration: params.expiration.get().map(|x| {
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
}),
expired: params.is_expired(now),
scope: params.scope.get().0.clone(),
}
}
async fn get_existing_admin_token(garage: &Garage, id: &String) -> Result<AdminApiToken, Error> {
garage
.admin_token_table
.get(&EmptyKey, id)
.await?
.filter(|k| !k.state.is_deleted())
.ok_or_else(|| Error::NoSuchAdminToken(id.to_string()))
}
fn apply_token_updates(
token: &mut AdminApiToken,
updates: UpdateAdminTokenRequestBody,
) -> Result<(), Error> {
if updates.never_expires && updates.expiration.is_some() {
return Err(Error::bad_request(
"cannot specify `expiration` and `never_expires`",
));
}
let params = token.params_mut().unwrap();
if let Some(name) = updates.name {
params.name.update(name);
}
if let Some(expiration) = updates.expiration {
params
.expiration
.update(Some(expiration.timestamp_millis() as u64));
}
if updates.never_expires {
params.expiration.update(None);
}
if let Some(scope) = updates.scope {
params.scope.update(AdminApiTokenScope(scope));
}
Ok(())
}
+1312
View File
File diff suppressed because it is too large Load Diff
+228 -266
View File
@@ -1,335 +1,230 @@
use std::collections::HashMap;
use std::borrow::Cow;
use std::sync::Arc;
use argon2::password_hash::PasswordHash;
use async_trait::async_trait;
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION};
use hyper::{body::Incoming as IncomingBody, Request, Response};
use serde::{Deserialize, Serialize};
use tokio::sync::watch;
use opentelemetry::trace::SpanRef;
#[cfg(feature = "metrics")]
use opentelemetry_prometheus::PrometheusExporter;
#[cfg(feature = "metrics")]
use prometheus::{Encoder, TextEncoder};
use garage_model::garage::Garage;
use garage_rpc::system::ClusterHealthStatus;
use garage_rpc::{Endpoint as RpcEndpoint, *};
use garage_table::EmptyKey;
use garage_util::background::BackgroundRunner;
use garage_util::data::Uuid;
use garage_util::error::Error as GarageError;
use garage_util::socket_address::UnixOrTCPSocketAddress;
use garage_util::time::now_msec;
use crate::generic_server::*;
use garage_api_common::generic_server::*;
use garage_api_common::helpers::*;
use crate::admin::bucket::*;
use crate::admin::cluster::*;
use crate::admin::error::*;
use crate::admin::key::*;
use crate::admin::router_v0;
use crate::admin::router_v1::{Authorization, Endpoint};
use crate::helpers::*;
use crate::api::*;
use crate::error::*;
use crate::router_v0;
use crate::router_v1;
use crate::Authorization;
use crate::RequestHandler;
// ---- FOR RPC ----
pub const ADMIN_RPC_PATH: &str = "garage_api/admin/rpc.rs/Rpc";
#[derive(Debug, Serialize, Deserialize)]
pub enum AdminRpc {
Proxy(AdminApiRequest),
Internal(LocalAdminApiRequest),
}
#[derive(Debug, Serialize, Deserialize)]
pub enum AdminRpcResponse {
ProxyApiOkResponse(TaggedAdminApiResponse),
InternalApiOkResponse(LocalAdminApiResponse),
ApiErrorResponse {
http_code: u16,
error_code: String,
message: String,
},
}
impl Rpc for AdminRpc {
type Response = Result<AdminRpcResponse, GarageError>;
}
impl EndpointHandler<AdminRpc> for AdminApiServer {
async fn handle(
self: &Arc<Self>,
message: &AdminRpc,
_from: NodeID,
) -> Result<AdminRpcResponse, GarageError> {
match message {
AdminRpc::Proxy(req) => {
info!("Proxied admin API request: {}", req.name());
let res = req.clone().handle(&self.garage, &self).await;
match res {
Ok(res) => Ok(AdminRpcResponse::ProxyApiOkResponse(res.tagged())),
Err(e) => Ok(AdminRpcResponse::ApiErrorResponse {
http_code: e.http_status_code().as_u16(),
error_code: e.code().to_string(),
message: e.to_string(),
}),
}
}
AdminRpc::Internal(req) => {
info!("Internal admin API request: {}", req.name());
let res = req.clone().handle(&self.garage, &self).await;
match res {
Ok(res) => Ok(AdminRpcResponse::InternalApiOkResponse(res)),
Err(e) => Ok(AdminRpcResponse::ApiErrorResponse {
http_code: e.http_status_code().as_u16(),
error_code: e.code().to_string(),
message: e.to_string(),
}),
}
}
}
}
}
// ---- FOR HTTP ----
pub type ResBody = BoxBody<Error>;
pub struct AdminApiServer {
garage: Arc<Garage>,
#[cfg(feature = "metrics")]
exporter: PrometheusExporter,
pub(crate) exporter: PrometheusExporter,
metrics_token: Option<String>,
metrics_require_token: bool,
admin_token: Option<String>,
pub(crate) background: Arc<BackgroundRunner>,
pub(crate) endpoint: Arc<RpcEndpoint<AdminRpc, Self>>,
}
pub enum HttpEndpoint {
Old(router_v1::Endpoint),
New(String),
}
impl AdminApiServer {
pub fn new(
garage: Arc<Garage>,
background: Arc<BackgroundRunner>,
#[cfg(feature = "metrics")] exporter: PrometheusExporter,
) -> Self {
) -> Arc<Self> {
let cfg = &garage.config.admin;
let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token);
let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token);
Self {
let metrics_require_token = cfg.metrics_require_token;
let endpoint = garage.system.netapp.endpoint(ADMIN_RPC_PATH.into());
let admin = Arc::new(Self {
garage,
#[cfg(feature = "metrics")]
exporter,
metrics_token,
metrics_require_token,
admin_token,
}
background,
endpoint,
});
admin.endpoint.set_handler(admin.clone());
admin
}
pub async fn run(
self,
self: Arc<Self>,
bind_addr: UnixOrTCPSocketAddress,
must_exit: watch::Receiver<bool>,
) -> Result<(), GarageError> {
let region = self.garage.config.s3_api.s3_region.clone();
ApiServer::new(region, self)
ApiServer::new(region, ArcAdminApiServer(self))
.run_server(bind_addr, Some(0o220), must_exit)
.await
}
fn handle_options(&self, _req: &Request<IncomingBody>) -> Result<Response<ResBody>, Error> {
Ok(Response::builder()
.status(StatusCode::NO_CONTENT)
.header(ALLOW, "OPTIONS, GET, POST")
.header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST")
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(empty_body())?)
}
async fn handle_check_domain(
async fn handle_http_api(
&self,
req: Request<IncomingBody>,
endpoint: HttpEndpoint,
) -> Result<Response<ResBody>, Error> {
let query_params: HashMap<String, String> = req
.uri()
.query()
.map(|v| {
url::form_urlencoded::parse(v.as_bytes())
.into_owned()
.collect()
})
.unwrap_or_else(HashMap::new);
let auth_header = req.headers().get(AUTHORIZATION).cloned();
let has_domain_key = query_params.contains_key("domain");
if !has_domain_key {
return Err(Error::bad_request("No domain query string found"));
}
let domain = query_params
.get("domain")
.ok_or_internal_error("Could not parse domain query string")?;
if self.check_domain(domain).await? {
Ok(Response::builder()
.status(StatusCode::OK)
.body(string_body(format!(
"Domain '{domain}' is managed by Garage"
)))?)
} else {
Err(Error::bad_request(format!(
"Domain '{domain}' is not managed by Garage"
)))
}
}
async fn check_domain(&self, domain: &str) -> Result<bool, Error> {
// Resolve bucket from domain name, inferring if the website must be activated for the
// domain to be valid.
let (bucket_name, must_check_website) = if let Some(bname) = self
.garage
.config
.s3_api
.root_domain
.as_ref()
.and_then(|rd| host_to_bucket(domain, rd))
{
(bname.to_string(), false)
} else if let Some(bname) = self
.garage
.config
.s3_web
.as_ref()
.and_then(|sw| host_to_bucket(domain, sw.root_domain.as_str()))
{
(bname.to_string(), true)
} else {
(domain.to_string(), true)
let request = match endpoint {
HttpEndpoint::Old(endpoint_v1) => AdminApiRequest::from_v1(endpoint_v1, req).await?,
HttpEndpoint::New(_) => AdminApiRequest::from_request(req).await?,
};
let bucket_id = match self
.garage
.bucket_helper()
.resolve_global_bucket_name(&bucket_name)
.await?
{
Some(bucket_id) => bucket_id,
None => return Ok(false),
};
if !must_check_website {
return Ok(true);
}
let bucket = self
.garage
.bucket_helper()
.get_existing_bucket(bucket_id)
.await?;
let bucket_state = bucket.state.as_option().unwrap();
let bucket_website_config = bucket_state.website_config.get();
match bucket_website_config {
Some(_v) => Ok(true),
None => Ok(false),
}
}
fn handle_health(&self) -> Result<Response<ResBody>, Error> {
let health = self.garage.system.health();
let (status, status_str) = match health.status {
ClusterHealthStatus::Healthy => (StatusCode::OK, "Garage is fully operational"),
ClusterHealthStatus::Degraded => (
StatusCode::OK,
"Garage is operational but some storage nodes are unavailable",
),
ClusterHealthStatus::Unavailable => (
StatusCode::SERVICE_UNAVAILABLE,
"Quorum is not available for some/all partitions, reads and writes will fail",
let (global_token_hash, token_required) = match request.authorization_type() {
Authorization::None => (None, false),
Authorization::MetricsToken => (
self.metrics_token.as_deref(),
self.metrics_token.is_some() || self.metrics_require_token,
),
Authorization::AdminToken => (self.admin_token.as_deref(), true),
};
let status_str = format!(
"{}\nConsult the full health check API endpoint at /v1/health for more details\n",
status_str
);
Ok(Response::builder()
.status(status)
.header(http::header::CONTENT_TYPE, "text/plain")
.body(string_body(status_str))?)
}
fn handle_metrics(&self) -> Result<Response<ResBody>, Error> {
#[cfg(feature = "metrics")]
{
use opentelemetry::trace::Tracer;
let mut buffer = vec![];
let encoder = TextEncoder::new();
let tracer = opentelemetry::global::tracer("garage");
let metric_families = tracer.in_span("admin/gather_metrics", |_| {
self.exporter.registry().gather()
});
encoder
.encode(&metric_families, &mut buffer)
.ok_or_internal_error("Could not serialize metrics")?;
Ok(Response::builder()
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, encoder.format_type())
.body(bytes_body(buffer.into()))?)
if token_required {
verify_authorization(&self.garage, global_token_hash, auth_header, request.name())?;
}
match request {
AdminApiRequest::Options(req) => req.handle(&self.garage, &self).await,
AdminApiRequest::CheckDomain(req) => req.handle(&self.garage, &self).await,
AdminApiRequest::Health(req) => req.handle(&self.garage, &self).await,
AdminApiRequest::Metrics(req) => req.handle(&self.garage, &self).await,
req => {
let res = req.handle(&self.garage, &self).await?;
let mut res = json_ok_response(&res)?;
res.headers_mut()
.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
Ok(res)
}
}
#[cfg(not(feature = "metrics"))]
Err(Error::bad_request(
"Garage was built without the metrics feature".to_string(),
))
}
}
#[async_trait]
impl ApiHandler for AdminApiServer {
struct ArcAdminApiServer(Arc<AdminApiServer>);
impl ApiHandler for ArcAdminApiServer {
const API_NAME: &'static str = "admin";
const API_NAME_DISPLAY: &'static str = "Admin";
type Endpoint = Endpoint;
type Endpoint = HttpEndpoint;
type Error = Error;
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<Endpoint, Error> {
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<HttpEndpoint, Error> {
if req.uri().path().starts_with("/v0/") {
let endpoint_v0 = router_v0::Endpoint::from_request(req)?;
Endpoint::from_v0(endpoint_v0)
let endpoint_v1 = router_v1::Endpoint::from_v0(endpoint_v0)?;
Ok(HttpEndpoint::Old(endpoint_v1))
} else if req.uri().path().starts_with("/v1/") {
let endpoint_v1 = router_v1::Endpoint::from_request(req)?;
Ok(HttpEndpoint::Old(endpoint_v1))
} else {
Endpoint::from_request(req)
Ok(HttpEndpoint::New(req.uri().path().to_string()))
}
}
async fn handle(
&self,
req: Request<IncomingBody>,
endpoint: Endpoint,
endpoint: HttpEndpoint,
) -> Result<Response<ResBody>, Error> {
let required_auth_hash =
match endpoint.authorization_type() {
Authorization::None => None,
Authorization::MetricsToken => self.metrics_token.as_deref(),
Authorization::AdminToken => match self.admin_token.as_deref() {
None => return Err(Error::forbidden(
"Admin token isn't configured, admin API access is disabled for security.",
)),
Some(t) => Some(t),
},
};
if let Some(password_hash) = required_auth_hash {
match req.headers().get("Authorization") {
None => return Err(Error::forbidden("Authorization token must be provided")),
Some(authorization) => {
verify_bearer_token(&authorization, password_hash)?;
}
}
}
match endpoint {
Endpoint::Options => self.handle_options(&req),
Endpoint::CheckDomain => self.handle_check_domain(req).await,
Endpoint::Health => self.handle_health(),
Endpoint::Metrics => self.handle_metrics(),
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,
Endpoint::GetClusterHealth => handle_get_cluster_health(&self.garage).await,
Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await,
// Layout
Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await,
Endpoint::UpdateClusterLayout => handle_update_cluster_layout(&self.garage, req).await,
Endpoint::ApplyClusterLayout => handle_apply_cluster_layout(&self.garage, req).await,
Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage, req).await,
// Keys
Endpoint::ListKeys => handle_list_keys(&self.garage).await,
Endpoint::GetKeyInfo {
id,
search,
show_secret_key,
} => {
let show_secret_key = show_secret_key.map(|x| x == "true").unwrap_or(false);
handle_get_key_info(&self.garage, id, search, show_secret_key).await
}
Endpoint::CreateKey => handle_create_key(&self.garage, req).await,
Endpoint::ImportKey => handle_import_key(&self.garage, req).await,
Endpoint::UpdateKey { id } => handle_update_key(&self.garage, id, req).await,
Endpoint::DeleteKey { id } => handle_delete_key(&self.garage, id).await,
// Buckets
Endpoint::ListBuckets => handle_list_buckets(&self.garage).await,
Endpoint::GetBucketInfo { id, global_alias } => {
handle_get_bucket_info(&self.garage, id, global_alias).await
}
Endpoint::CreateBucket => handle_create_bucket(&self.garage, req).await,
Endpoint::DeleteBucket { id } => handle_delete_bucket(&self.garage, id).await,
Endpoint::UpdateBucket { id } => handle_update_bucket(&self.garage, id, req).await,
// Bucket-key permissions
Endpoint::BucketAllowKey => {
handle_bucket_change_key_perm(&self.garage, req, true).await
}
Endpoint::BucketDenyKey => {
handle_bucket_change_key_perm(&self.garage, req, false).await
}
// Bucket aliasing
Endpoint::GlobalAliasBucket { id, alias } => {
handle_global_alias_bucket(&self.garage, id, alias).await
}
Endpoint::GlobalUnaliasBucket { id, alias } => {
handle_global_unalias_bucket(&self.garage, id, alias).await
}
Endpoint::LocalAliasBucket {
id,
access_key_id,
alias,
} => handle_local_alias_bucket(&self.garage, id, access_key_id, alias).await,
Endpoint::LocalUnaliasBucket {
id,
access_key_id,
alias,
} => handle_local_unalias_bucket(&self.garage, id, access_key_id, alias).await,
}
self.0.handle_http_api(req, endpoint).await
}
}
impl ApiEndpoint for Endpoint {
fn name(&self) -> &'static str {
Endpoint::name(self)
impl ApiEndpoint for HttpEndpoint {
fn name(&self) -> Cow<'static, str> {
match self {
Self::Old(endpoint_v1) => Cow::Borrowed(endpoint_v1.name()),
Self::New(path) => Cow::Owned(path.clone()),
}
}
fn add_span_attributes(&self, _span: SpanRef<'_>) {}
@@ -349,20 +244,87 @@ fn hash_bearer_token(token: &str) -> String {
.to_string()
}
fn verify_bearer_token(token: &hyper::http::HeaderValue, password_hash: &str) -> Result<(), Error> {
use argon2::{password_hash::PasswordVerifier, Argon2};
fn verify_authorization(
garage: &Garage,
global_token_hash: Option<&str>,
auth_header: Option<hyper::http::HeaderValue>,
endpoint_name: &str,
) -> Result<(), Error> {
use argon2::{password_hash::PasswordHash, password_hash::PasswordVerifier, Argon2};
let parsed_hash = PasswordHash::new(&password_hash).unwrap();
let invalid_msg = "Invalid bearer token";
token
.to_str()?
.strip_prefix("Bearer ")
.and_then(|token| {
Argon2::default()
.verify_password(token.trim().as_bytes(), &parsed_hash)
.ok()
})
.ok_or_else(|| Error::forbidden("Invalid authorization token"))?;
let token = match &auth_header {
None => {
return Err(Error::forbidden(
"Bearer token must be provided in Authorization header",
))
}
Some(authorization) => authorization
.to_str()?
.strip_prefix("Bearer ")
.ok_or_else(|| Error::forbidden("Invalid Authorization header"))?
.trim(),
};
let token_hash_string = if let Some((prefix, _)) = token.split_once('.') {
garage
.admin_token_table
.get_local(&EmptyKey, &prefix.to_string())?
.and_then(|k| k.state.into_option())
.filter(|p| !p.is_expired(now_msec()))
.filter(|p| p.has_scope(endpoint_name))
.ok_or_else(|| Error::forbidden(invalid_msg))?
.token_hash
} else {
global_token_hash
.ok_or_else(|| Error::forbidden(invalid_msg))?
.to_string()
};
let token_hash =
PasswordHash::new(&token_hash_string).ok_or_internal_error("Could not parse token hash")?;
Argon2::default()
.verify_password(token.as_bytes(), &token_hash)
.map_err(|_| Error::forbidden(invalid_msg))?;
Ok(())
}
pub(crate) fn find_matching_nodes(garage: &Garage, spec: &str) -> Result<Vec<Uuid>, Error> {
let mut res = vec![];
if spec == "*" {
res = garage.system.cluster_layout().all_nodes().to_vec();
for node in garage.system.get_known_nodes() {
if node.is_up && !res.contains(&node.id) {
res.push(node.id);
}
}
} else if spec == "self" {
res.push(garage.system.id);
} else {
let layout = garage.system.cluster_layout();
let known_nodes = garage.system.get_known_nodes();
let all_nodes = layout
.all_nodes()
.iter()
.copied()
.chain(known_nodes.iter().filter(|x| x.is_up).map(|x| x.id));
for node in all_nodes {
if !res.contains(&node) && hex::encode(node).starts_with(spec) {
res.push(node);
}
}
if res.is_empty() {
return Err(Error::bad_request(format!("No nodes matching {}", spec)));
}
if res.len() > 1 {
return Err(Error::bad_request(format!(
"Multiple nodes matching {}: {:?}",
spec, res
)));
}
}
Ok(res)
}
+276
View File
@@ -0,0 +1,276 @@
use std::sync::Arc;
use garage_util::data::*;
use garage_util::error::Error as GarageError;
use garage_util::time::now_msec;
use garage_table::EmptyKey;
use garage_model::garage::Garage;
use garage_model::s3::object_table::*;
use garage_model::s3::version_table::*;
use garage_api_common::common_error::CommonErrorDerivative;
use crate::api::*;
use crate::error::*;
use crate::{Admin, RequestHandler};
impl RequestHandler for LocalListBlockErrorsRequest {
type Response = LocalListBlockErrorsResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<LocalListBlockErrorsResponse, Error> {
let errors = garage.block_manager.list_resync_errors()?;
let now = now_msec();
let errors = errors
.into_iter()
.map(|e| BlockError {
block_hash: hex::encode(&e.hash),
refcount: e.refcount,
error_count: e.error_count,
last_try_secs_ago: now.saturating_sub(e.last_try) / 1000,
next_try_in_secs: e.next_try.saturating_sub(now) / 1000,
})
.collect();
Ok(LocalListBlockErrorsResponse(errors))
}
}
impl RequestHandler for LocalGetBlockInfoRequest {
type Response = LocalGetBlockInfoResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<LocalGetBlockInfoResponse, Error> {
let hash = find_block_hash_by_prefix(garage, &self.block_hash)?;
let refcount = garage.block_manager.get_block_rc(&hash)?;
let block_refs = garage
.block_ref_table
.get_range(&hash, None, None, 10000, Default::default())
.await?;
let mut versions = vec![];
for br in block_refs {
if let Some(v) = garage.version_table.get(&br.version, &EmptyKey).await? {
let bl = match &v.backlink {
VersionBacklink::MultipartUpload { upload_id } => {
if let Some(u) = garage.mpu_table.get(upload_id, &EmptyKey).await? {
BlockVersionBacklink::Upload {
upload_id: hex::encode(&upload_id),
upload_deleted: u.deleted.get(),
upload_garbage_collected: false,
bucket_id: Some(hex::encode(&u.bucket_id)),
key: Some(u.key.to_string()),
}
} else {
BlockVersionBacklink::Upload {
upload_id: hex::encode(&upload_id),
upload_deleted: true,
upload_garbage_collected: true,
bucket_id: None,
key: None,
}
}
}
VersionBacklink::Object { bucket_id, key } => BlockVersionBacklink::Object {
bucket_id: hex::encode(&bucket_id),
key: key.to_string(),
},
};
versions.push(BlockVersion {
version_id: hex::encode(&br.version),
ref_deleted: br.deleted.get(),
version_deleted: v.deleted.get(),
garbage_collected: false,
backlink: Some(bl),
});
} else {
versions.push(BlockVersion {
version_id: hex::encode(&br.version),
ref_deleted: br.deleted.get(),
version_deleted: true,
garbage_collected: true,
backlink: None,
});
}
}
Ok(LocalGetBlockInfoResponse {
block_hash: hex::encode(&hash),
refcount,
versions,
})
}
}
impl RequestHandler for LocalRetryBlockResyncRequest {
type Response = LocalRetryBlockResyncResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<LocalRetryBlockResyncResponse, Error> {
match self {
Self::All { all: true } => {
let blocks = garage.block_manager.list_resync_errors()?;
for b in blocks.iter() {
garage.block_manager.resync.clear_backoff(&b.hash)?;
}
Ok(LocalRetryBlockResyncResponse {
count: blocks.len() as u64,
})
}
Self::All { all: false } => Err(Error::bad_request("nonsense")),
Self::Blocks { block_hashes } => {
for hash in block_hashes.iter() {
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
let hash = Hash::try_from(&hash).ok_or_bad_request("invalid hash")?;
garage.block_manager.resync.clear_backoff(&hash)?;
}
Ok(LocalRetryBlockResyncResponse {
count: block_hashes.len() as u64,
})
}
}
}
}
impl RequestHandler for LocalPurgeBlocksRequest {
type Response = LocalPurgeBlocksResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<LocalPurgeBlocksResponse, Error> {
let mut obj_dels = 0;
let mut mpu_dels = 0;
let mut ver_dels = 0;
for hash in self.0.iter() {
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
let hash = Hash::try_from(&hash).ok_or_bad_request("invalid hash")?;
let block_refs = garage
.block_ref_table
.get_range(&hash, None, None, 10000, Default::default())
.await?;
for br in block_refs {
if let Some(version) = garage.version_table.get(&br.version, &EmptyKey).await? {
handle_block_purge_version_backlink(
garage,
&version,
&mut obj_dels,
&mut mpu_dels,
)
.await?;
if !version.deleted.get() {
let deleted_version = Version::new(version.uuid, version.backlink, true);
garage.version_table.insert(&deleted_version).await?;
ver_dels += 1;
}
}
}
}
Ok(LocalPurgeBlocksResponse {
blocks_purged: self.0.len() as u64,
versions_deleted: ver_dels,
objects_deleted: obj_dels,
uploads_deleted: mpu_dels,
})
}
}
fn find_block_hash_by_prefix(garage: &Arc<Garage>, prefix: &str) -> Result<Hash, Error> {
if prefix.len() < 4 {
return Err(Error::bad_request(
"Please specify at least 4 characters of the block hash",
));
}
let prefix_bin = hex::decode(&prefix[..prefix.len() & !1]).ok_or_bad_request("invalid hash")?;
let iter = garage
.block_ref_table
.data
.store
.range(&prefix_bin[..]..)
.map_err(GarageError::from)?;
let mut found = None;
for item in iter {
let (k, _v) = item.map_err(GarageError::from)?;
let hash = Hash::try_from(&k[..32]).unwrap();
if &hash.as_slice()[..prefix_bin.len()] != prefix_bin {
break;
}
if hex::encode(hash.as_slice()).starts_with(prefix) {
match &found {
Some(x) if *x == hash => (),
Some(_) => {
return Err(Error::bad_request(format!(
"Several blocks match prefix `{}`",
prefix
)));
}
None => {
found = Some(hash);
}
}
}
}
found.ok_or_else(|| Error::NoSuchBlock(prefix.to_string()))
}
async fn handle_block_purge_version_backlink(
garage: &Arc<Garage>,
version: &Version,
obj_dels: &mut u64,
mpu_dels: &mut u64,
) -> Result<(), Error> {
let (bucket_id, key, ov_id) = match &version.backlink {
VersionBacklink::Object { bucket_id, key } => (*bucket_id, key.clone(), version.uuid),
VersionBacklink::MultipartUpload { upload_id } => {
if let Some(mut mpu) = garage.mpu_table.get(upload_id, &EmptyKey).await? {
if !mpu.deleted.get() {
mpu.parts.clear();
mpu.deleted.set();
garage.mpu_table.insert(&mpu).await?;
*mpu_dels += 1;
}
(mpu.bucket_id, mpu.key.clone(), *upload_id)
} else {
return Ok(());
}
}
};
if let Some(object) = garage.object_table.get(&bucket_id, &key).await? {
let ov = object.versions().iter().rev().find(|v| v.is_complete());
if let Some(ov) = ov {
if ov.uuid == ov_id {
let del_uuid = gen_uuid();
let deleted_object = Object::new(
bucket_id,
key,
vec![ObjectVersion {
uuid: del_uuid,
timestamp: ov.timestamp + 1,
state: ObjectVersionState::Complete(ObjectVersionData::DeleteMarker),
}],
);
garage.object_table.insert(&deleted_object).await?;
*obj_dels += 1;
}
}
}
Ok(())
}
+648 -488
View File
File diff suppressed because it is too large Load Diff
+243 -275
View File
@@ -1,308 +1,276 @@
use std::net::SocketAddr;
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::Arc;
use hyper::{body::Incoming as IncomingBody, Request, Response};
use serde::{Deserialize, Serialize};
use format_table::format_table_to_string;
use garage_util::crdt::*;
use garage_util::data::*;
use garage_rpc::layout;
use garage_rpc::layout::PARTITION_BITS;
use garage_model::garage::Garage;
use crate::admin::api_server::ResBody;
use crate::admin::error::*;
use crate::helpers::{json_ok_response, parse_json_body};
use crate::api::*;
use crate::error::*;
use crate::{Admin, RequestHandler};
pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
let res = GetClusterStatusResponse {
node: hex::encode(garage.system.id),
garage_version: garage_util::version::garage_version(),
garage_features: garage_util::version::garage_features(),
rust_version: garage_util::version::rust_version(),
db_engine: garage.db.engine(),
known_nodes: garage
impl RequestHandler for GetClusterStatusRequest {
type Response = GetClusterStatusResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<GetClusterStatusResponse, Error> {
let layout = garage.system.cluster_layout();
let mut nodes = garage
.system
.get_known_nodes()
.into_iter()
.map(|i| KnownNodeResp {
id: hex::encode(i.id),
addr: i.addr,
is_up: i.is_up,
last_seen_secs_ago: i.last_seen_secs_ago,
hostname: i.status.hostname,
.map(|i| {
(
i.id,
NodeResp {
id: hex::encode(i.id),
garage_version: i.status.garage_version,
addr: i.addr,
hostname: i.status.hostname,
is_up: i.is_up,
last_seen_secs_ago: i.last_seen_secs_ago,
data_partition: i.status.data_disk_avail.map(|(avail, total)| {
FreeSpaceResp {
available: avail,
total,
}
}),
metadata_partition: i.status.meta_disk_avail.map(|(avail, total)| {
FreeSpaceResp {
available: avail,
total,
}
}),
..Default::default()
},
)
})
.collect(),
layout: format_cluster_layout(&garage.system.get_cluster_layout()),
};
.collect::<HashMap<_, _>>();
Ok(json_ok_response(&res)?)
}
pub async fn handle_get_cluster_health(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
use garage_rpc::system::ClusterHealthStatus;
let health = garage.system.health();
let health = ClusterHealth {
status: match health.status {
ClusterHealthStatus::Healthy => "healthy",
ClusterHealthStatus::Degraded => "degraded",
ClusterHealthStatus::Unavailable => "unavailable",
},
known_nodes: health.known_nodes,
connected_nodes: health.connected_nodes,
storage_nodes: health.storage_nodes,
storage_nodes_ok: health.storage_nodes_ok,
partitions: health.partitions,
partitions_quorum: health.partitions_quorum,
partitions_all_ok: health.partitions_all_ok,
};
Ok(json_ok_response(&health)?)
}
pub async fn handle_connect_cluster_nodes(
garage: &Arc<Garage>,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
let req = parse_json_body::<Vec<String>, _, Error>(req).await?;
let res = futures::future::join_all(req.iter().map(|node| garage.system.connect(node)))
.await
.into_iter()
.map(|r| match r {
Ok(()) => ConnectClusterNodesResponse {
success: true,
error: None,
},
Err(e) => ConnectClusterNodesResponse {
success: false,
error: Some(format!("{}", e)),
},
})
.collect::<Vec<_>>();
Ok(json_ok_response(&res)?)
}
pub async fn handle_get_cluster_layout(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
let res = format_cluster_layout(&garage.system.get_cluster_layout());
Ok(json_ok_response(&res)?)
}
fn format_cluster_layout(layout: &layout::ClusterLayout) -> GetClusterLayoutResponse {
let roles = layout
.roles
.items()
.iter()
.filter_map(|(k, _, v)| v.0.clone().map(|x| (k, x)))
.map(|(k, v)| NodeRoleResp {
id: hex::encode(k),
zone: v.zone.clone(),
capacity: v.capacity,
tags: v.tags.clone(),
})
.collect::<Vec<_>>();
let staged_role_changes = layout
.staging_roles
.items()
.iter()
.filter(|(k, _, v)| layout.roles.get(k) != Some(v))
.map(|(k, _, v)| match &v.0 {
None => NodeRoleChange {
id: hex::encode(k),
action: NodeRoleChangeEnum::Remove { remove: true },
},
Some(r) => NodeRoleChange {
id: hex::encode(k),
action: NodeRoleChangeEnum::Update {
zone: r.zone.clone(),
for (id, _, role) in layout.current().roles.items().iter() {
if let layout::NodeRoleV(Some(r)) = role {
let role = NodeAssignedRole {
zone: r.zone.to_string(),
capacity: r.capacity,
tags: r.tags.clone(),
},
},
};
match nodes.get_mut(id) {
None => {
nodes.insert(
*id,
NodeResp {
id: hex::encode(id),
role: Some(role),
..Default::default()
},
);
}
Some(n) => {
n.role = Some(role);
}
}
}
}
for ver in layout.versions().iter().rev().skip(1) {
for (id, _, role) in ver.roles.items().iter() {
if let layout::NodeRoleV(Some(r)) = role {
if r.capacity.is_some() {
if let Some(n) = nodes.get_mut(id) {
if n.role.is_none() {
n.draining = true;
}
} else {
nodes.insert(
*id,
NodeResp {
id: hex::encode(id),
draining: true,
..Default::default()
},
);
}
}
}
}
}
let mut nodes = nodes.into_values().collect::<Vec<_>>();
nodes.sort_by(|x, y| x.id.cmp(&y.id));
Ok(GetClusterStatusResponse {
layout_version: layout.current().version,
nodes,
})
.collect::<Vec<_>>();
GetClusterLayoutResponse {
version: layout.version,
roles,
staged_role_changes,
}
}
// ----
impl RequestHandler for GetClusterHealthRequest {
type Response = GetClusterHealthResponse;
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClusterHealth {
status: &'static str,
known_nodes: usize,
connected_nodes: usize,
storage_nodes: usize,
storage_nodes_ok: usize,
partitions: usize,
partitions_quorum: usize,
partitions_all_ok: usize,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetClusterStatusResponse {
node: String,
garage_version: &'static str,
garage_features: Option<&'static [&'static str]>,
rust_version: &'static str,
db_engine: String,
known_nodes: Vec<KnownNodeResp>,
layout: GetClusterLayoutResponse,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ApplyClusterLayoutResponse {
message: Vec<String>,
layout: GetClusterLayoutResponse,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ConnectClusterNodesResponse {
success: bool,
error: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetClusterLayoutResponse {
version: u64,
roles: Vec<NodeRoleResp>,
staged_role_changes: Vec<NodeRoleChange>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct NodeRoleResp {
id: String,
zone: String,
capacity: Option<u64>,
tags: Vec<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct KnownNodeResp {
id: String,
addr: SocketAddr,
is_up: bool,
last_seen_secs_ago: Option<u64>,
hostname: String,
}
// ---- update functions ----
pub async fn handle_update_cluster_layout(
garage: &Arc<Garage>,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
let updates = parse_json_body::<UpdateClusterLayoutRequest, _, Error>(req).await?;
let mut layout = garage.system.get_cluster_layout();
let mut roles = layout.roles.clone();
roles.merge(&layout.staging_roles);
for change in updates {
let node = hex::decode(&change.id).ok_or_bad_request("Invalid node identifier")?;
let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?;
let new_role = match change.action {
NodeRoleChangeEnum::Remove { remove: true } => None,
NodeRoleChangeEnum::Update {
zone,
capacity,
tags,
} => Some(layout::NodeRole {
zone,
capacity,
tags,
}),
_ => return Err(Error::bad_request("Invalid layout change")),
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<GetClusterHealthResponse, Error> {
use garage_rpc::system::ClusterHealthStatus;
let health = garage.system.health();
let health = GetClusterHealthResponse {
status: match health.status {
ClusterHealthStatus::Healthy => "healthy",
ClusterHealthStatus::Degraded => "degraded",
ClusterHealthStatus::Unavailable => "unavailable",
}
.to_string(),
known_nodes: health.known_nodes,
connected_nodes: health.connected_nodes,
storage_nodes: health.storage_nodes,
storage_nodes_ok: health.storage_nodes_ok,
partitions: health.partitions,
partitions_quorum: health.partitions_quorum,
partitions_all_ok: health.partitions_all_ok,
};
layout
.staging_roles
.merge(&roles.update_mutator(node, layout::NodeRoleV(new_role)));
Ok(health)
}
garage.system.update_cluster_layout(&layout).await?;
let res = format_cluster_layout(&layout);
Ok(json_ok_response(&res)?)
}
pub async fn handle_apply_cluster_layout(
garage: &Arc<Garage>,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
let param = parse_json_body::<ApplyRevertLayoutRequest, _, Error>(req).await?;
impl RequestHandler for GetClusterStatisticsRequest {
type Response = GetClusterStatisticsResponse;
let layout = garage.system.get_cluster_layout();
let (layout, msg) = layout.apply_staged_changes(Some(param.version))?;
// FIXME: return this as a JSON struct instead of text
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<GetClusterStatisticsResponse, Error> {
let mut ret = String::new();
garage.system.update_cluster_layout(&layout).await?;
// Gather storage node and free space statistics for current nodes
let layout = &garage.system.cluster_layout();
let mut node_partition_count = HashMap::<Uuid, u64>::new();
for short_id in layout.current().ring_assignment_data.iter() {
let id = layout.current().node_id_vec[*short_id as usize];
*node_partition_count.entry(id).or_default() += 1;
}
let node_info = garage
.system
.get_known_nodes()
.into_iter()
.map(|n| (n.id, n))
.collect::<HashMap<_, _>>();
let res = ApplyClusterLayoutResponse {
message: msg,
layout: format_cluster_layout(&layout),
};
Ok(json_ok_response(&res)?)
let mut table = vec![" ID\tHostname\tZone\tCapacity\tPart.\tDataAvail\tMetaAvail".into()];
for (id, parts) in node_partition_count.iter() {
let info = node_info.get(id);
let status = info.map(|x| &x.status);
let role = layout.current().roles.get(id).and_then(|x| x.0.as_ref());
let hostname = status.and_then(|x| x.hostname.as_deref()).unwrap_or("?");
let zone = role.map(|x| x.zone.as_str()).unwrap_or("?");
let capacity = role
.map(|x| x.capacity_string())
.unwrap_or_else(|| "?".into());
let avail_str = |x| match x {
Some((avail, total)) => {
let pct = (avail as f64) / (total as f64) * 100.;
let avail = bytesize::ByteSize::b(avail);
let total = bytesize::ByteSize::b(total);
format!("{}/{} ({:.1}%)", avail, total, pct)
}
None => "?".into(),
};
let data_avail = avail_str(status.and_then(|x| x.data_disk_avail));
let meta_avail = avail_str(status.and_then(|x| x.meta_disk_avail));
table.push(format!(
" {:?}\t{}\t{}\t{}\t{}\t{}\t{}",
id, hostname, zone, capacity, parts, data_avail, meta_avail
));
}
write!(
&mut ret,
"Storage nodes:\n{}",
format_table_to_string(table)
)
.unwrap();
let meta_part_avail = node_partition_count
.iter()
.filter_map(|(id, parts)| {
node_info
.get(id)
.and_then(|x| x.status.meta_disk_avail)
.map(|c| c.0 / *parts)
})
.collect::<Vec<_>>();
let data_part_avail = node_partition_count
.iter()
.filter_map(|(id, parts)| {
node_info
.get(id)
.and_then(|x| x.status.data_disk_avail)
.map(|c| c.0 / *parts)
})
.collect::<Vec<_>>();
if !meta_part_avail.is_empty() && !data_part_avail.is_empty() {
let meta_avail =
bytesize::ByteSize(meta_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS));
let data_avail =
bytesize::ByteSize(data_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS));
writeln!(
&mut ret,
"\nEstimated available storage space cluster-wide (might be lower in practice):"
)
.unwrap();
if meta_part_avail.len() < node_partition_count.len()
|| data_part_avail.len() < node_partition_count.len()
{
ret += &format_table_to_string(vec![
format!(" data: < {}", data_avail),
format!(" metadata: < {}", meta_avail),
]);
writeln!(&mut ret, "A precise estimate could not be given as information is missing for some storage nodes.").unwrap();
} else {
ret += &format_table_to_string(vec![
format!(" data: {}", data_avail),
format!(" metadata: {}", meta_avail),
]);
}
}
Ok(GetClusterStatisticsResponse { freeform: ret })
}
}
pub async fn handle_revert_cluster_layout(
garage: &Arc<Garage>,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
let param = parse_json_body::<ApplyRevertLayoutRequest, _, Error>(req).await?;
impl RequestHandler for ConnectClusterNodesRequest {
type Response = ConnectClusterNodesResponse;
let layout = garage.system.get_cluster_layout();
let layout = layout.revert_staged_changes(Some(param.version))?;
garage.system.update_cluster_layout(&layout).await?;
let res = format_cluster_layout(&layout);
Ok(json_ok_response(&res)?)
}
// ----
type UpdateClusterLayoutRequest = Vec<NodeRoleChange>;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApplyRevertLayoutRequest {
version: u64,
}
// ----
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct NodeRoleChange {
id: String,
#[serde(flatten)]
action: NodeRoleChangeEnum,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum NodeRoleChangeEnum {
#[serde(rename_all = "camelCase")]
Remove { remove: bool },
#[serde(rename_all = "camelCase")]
Update {
zone: String,
capacity: Option<u64>,
tags: Vec<String>,
},
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<ConnectClusterNodesResponse, Error> {
let res = futures::future::join_all(self.0.iter().map(|node| garage.system.connect(node)))
.await
.into_iter()
.map(|r| match r {
Ok(()) => ConnectNodeResponse {
success: true,
error: None,
},
Err(e) => ConnectNodeResponse {
success: false,
error: Some(format!("{}", e)),
},
})
.collect::<Vec<_>>();
Ok(ConnectClusterNodesResponse(res))
}
}
+47 -15
View File
@@ -1,26 +1,46 @@
use std::convert::TryFrom;
use err_derive::Error;
use hyper::header::HeaderValue;
use hyper::{HeaderMap, StatusCode};
pub use garage_model::helper::error::Error as HelperError;
use crate::common_error::CommonError;
pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError};
use crate::generic_server::ApiError;
use crate::helpers::*;
use garage_api_common::common_error::{commonErrorDerivative, CommonError};
pub use garage_api_common::common_error::{
CommonErrorDerivative, OkOrBadRequest, OkOrInternalError,
};
use garage_api_common::generic_server::ApiError;
use garage_api_common::helpers::*;
/// Errors of this crate
#[derive(Debug, Error)]
pub enum Error {
#[error(display = "{}", _0)]
/// Error from common error
Common(CommonError),
Common(#[error(source)] CommonError),
// Category: cannot process
/// The admin API token does not exist
#[error(display = "Admin token not found: {}", _0)]
NoSuchAdminToken(String),
/// The API access key does not exist
#[error(display = "Access key not found: {}", _0)]
NoSuchAccessKey(String),
/// The requested block does not exist
#[error(display = "Block not found: {}", _0)]
NoSuchBlock(String),
/// The requested worker does not exist
#[error(display = "Worker not found: {}", _0)]
NoSuchWorker(u64),
/// The object requested don't exists
#[error(display = "Key not found")]
NoSuchKey,
/// In Import key, the key already exists
#[error(
display = "Key {} already exists in data store. Even if it is deleted, we can't let you create a new key with the same ID. Sorry.",
@@ -29,23 +49,31 @@ pub enum Error {
KeyAlreadyExists(String),
}
impl<T> From<T> for Error
where
CommonError: From<T>,
{
fn from(err: T) -> Self {
Error::Common(CommonError::from(err))
commonErrorDerivative!(Error);
/// FIXME: helper errors are transformed into their corresponding variants
/// in the Error struct, but in many case a helper error should be considered
/// an internal error.
impl From<HelperError> for Error {
fn from(err: HelperError) -> Error {
match CommonError::try_from(err) {
Ok(ce) => Self::Common(ce),
Err(HelperError::NoSuchAccessKey(k)) => Self::NoSuchAccessKey(k),
Err(_) => unreachable!(),
}
}
}
impl CommonErrorDerivative for Error {}
impl Error {
fn code(&self) -> &'static str {
pub fn code(&self) -> &'static str {
match self {
Error::Common(c) => c.aws_code(),
Error::NoSuchAdminToken(_) => "NoSuchAdminToken",
Error::NoSuchAccessKey(_) => "NoSuchAccessKey",
Error::NoSuchWorker(_) => "NoSuchWorker",
Error::NoSuchBlock(_) => "NoSuchBlock",
Error::KeyAlreadyExists(_) => "KeyAlreadyExists",
Error::NoSuchKey => "NoSuchKey",
}
}
}
@@ -55,7 +83,11 @@ impl ApiError for Error {
fn http_status_code(&self) -> StatusCode {
match self {
Error::Common(c) => c.http_status_code(),
Error::NoSuchAccessKey(_) => StatusCode::NOT_FOUND,
Error::NoSuchAdminToken(_)
| Error::NoSuchAccessKey(_)
| Error::NoSuchWorker(_)
| Error::NoSuchBlock(_)
| Error::NoSuchKey => StatusCode::NOT_FOUND,
Error::KeyAlreadyExists(_) => StatusCode::CONFLICT,
}
}
+189 -168
View File
@@ -1,172 +1,190 @@
use std::collections::HashMap;
use std::sync::Arc;
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
use serde::{Deserialize, Serialize};
use chrono::DateTime;
use garage_table::*;
use garage_util::time::now_msec;
use garage_model::garage::Garage;
use garage_model::key_table::*;
use crate::admin::api_server::ResBody;
use crate::admin::error::*;
use crate::helpers::*;
use crate::api::*;
use crate::error::*;
use crate::{Admin, RequestHandler};
pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
let res = garage
.key_table
.get_range(
&EmptyKey,
None,
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
10000,
EnumerationOrder::Forward,
)
.await?
.iter()
.map(|k| ListKeyResultItem {
id: k.key_id.to_string(),
name: k.params().unwrap().name.get().clone(),
})
.collect::<Vec<_>>();
impl RequestHandler for ListKeysRequest {
type Response = ListKeysResponse;
Ok(json_ok_response(&res)?)
}
async fn handle(self, garage: &Arc<Garage>, _admin: &Admin) -> Result<ListKeysResponse, Error> {
let now = now_msec();
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ListKeyResultItem {
id: String,
name: String,
}
pub async fn handle_get_key_info(
garage: &Arc<Garage>,
id: Option<String>,
search: Option<String>,
show_secret_key: bool,
) -> Result<Response<ResBody>, Error> {
let key = if let Some(id) = id {
garage.key_helper().get_existing_key(&id).await?
} else if let Some(search) = search {
garage
.key_helper()
.get_existing_matching_key(&search)
let res = garage
.key_table
.get_range(
&EmptyKey,
None,
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
10000,
EnumerationOrder::Forward,
)
.await?
} else {
unreachable!();
};
.iter()
.map(|k| {
let p = k.params().unwrap();
key_info_results(garage, key, show_secret_key).await
}
ListKeysResponseItem {
id: k.key_id.to_string(),
name: p.name.get().clone(),
created: p.created.map(|x| {
DateTime::from_timestamp_millis(x as i64)
.expect("invalid timestamp stored in db")
}),
expiration: p.expiration.get().map(|x| {
DateTime::from_timestamp_millis(x as i64)
.expect("invalid timestamp stored in db")
}),
expired: p.is_expired(now),
}
})
.collect::<Vec<_>>();
pub async fn handle_create_key(
garage: &Arc<Garage>,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
let req = parse_json_body::<CreateKeyRequest, _, Error>(req).await?;
let key = Key::new(req.name.as_deref().unwrap_or("Unnamed key"));
garage.key_table.insert(&key).await?;
key_info_results(garage, key, true).await
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateKeyRequest {
name: Option<String>,
}
pub async fn handle_import_key(
garage: &Arc<Garage>,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
let req = parse_json_body::<ImportKeyRequest, _, Error>(req).await?;
let prev_key = garage.key_table.get(&EmptyKey, &req.access_key_id).await?;
if prev_key.is_some() {
return Err(Error::KeyAlreadyExists(req.access_key_id.to_string()));
Ok(ListKeysResponse(res))
}
let imported_key = Key::import(
&req.access_key_id,
&req.secret_access_key,
req.name.as_deref().unwrap_or("Imported key"),
)
.ok_or_bad_request("Invalid key format")?;
garage.key_table.insert(&imported_key).await?;
key_info_results(garage, imported_key, false).await
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ImportKeyRequest {
access_key_id: String,
secret_access_key: String,
name: Option<String>,
}
impl RequestHandler for GetKeyInfoRequest {
type Response = GetKeyInfoResponse;
pub async fn handle_update_key(
garage: &Arc<Garage>,
id: String,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
let req = parse_json_body::<UpdateKeyRequest, _, Error>(req).await?;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<GetKeyInfoResponse, Error> {
let key = match (self.id, self.search) {
(Some(id), None) => garage.key_helper().get_existing_key(&id).await?,
(None, Some(search)) => {
let candidates = garage
.key_table
.get_range(
&EmptyKey,
None,
Some(KeyFilter::MatchesAndNotDeleted(search.to_string())),
10,
EnumerationOrder::Forward,
)
.await?
.into_iter()
.collect::<Vec<_>>();
if candidates.len() != 1 {
return Err(Error::bad_request(format!(
"{} matching keys",
candidates.len()
)));
}
candidates.into_iter().next().unwrap()
}
_ => {
return Err(Error::bad_request(
"Either id or search must be provided (but not both)",
));
}
};
let mut key = garage.key_helper().get_existing_key(&id).await?;
let key_state = key.state.as_option_mut().unwrap();
if let Some(new_name) = req.name {
key_state.name.update(new_name);
Ok(key_info_results(garage, key, self.show_secret_key).await?)
}
if let Some(allow) = req.allow {
if allow.create_bucket {
key_state.allow_create_bucket.update(true);
}
impl RequestHandler for CreateKeyRequest {
type Response = CreateKeyResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<CreateKeyResponse, Error> {
let mut key = Key::new("Unnamed key");
apply_key_updates(&mut key, self.0)?;
garage.key_table.insert(&key).await?;
Ok(CreateKeyResponse(
key_info_results(garage, key, true).await?,
))
}
}
impl RequestHandler for ImportKeyRequest {
type Response = ImportKeyResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<ImportKeyResponse, Error> {
let prev_key = garage.key_table.get(&EmptyKey, &self.access_key_id).await?;
if prev_key.is_some() {
return Err(Error::KeyAlreadyExists(self.access_key_id.to_string()));
}
}
if let Some(deny) = req.deny {
if deny.create_bucket {
key_state.allow_create_bucket.update(false);
}
}
garage.key_table.insert(&key).await?;
let imported_key = Key::import(
&self.access_key_id,
&self.secret_access_key,
self.name.as_deref().unwrap_or("Imported key"),
)
.ok_or_bad_request("Invalid key format")?;
garage.key_table.insert(&imported_key).await?;
key_info_results(garage, key, false).await
Ok(ImportKeyResponse(
key_info_results(garage, imported_key, false).await?,
))
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct UpdateKeyRequest {
name: Option<String>,
allow: Option<KeyPerm>,
deny: Option<KeyPerm>,
impl RequestHandler for UpdateKeyRequest {
type Response = UpdateKeyResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<UpdateKeyResponse, Error> {
let mut key = garage.key_helper().get_existing_key(&self.id).await?;
apply_key_updates(&mut key, self.body)?;
garage.key_table.insert(&key).await?;
Ok(UpdateKeyResponse(
key_info_results(garage, key, false).await?,
))
}
}
pub async fn handle_delete_key(
garage: &Arc<Garage>,
id: String,
) -> Result<Response<ResBody>, Error> {
let helper = garage.locked_helper().await;
impl RequestHandler for DeleteKeyRequest {
type Response = DeleteKeyResponse;
let mut key = helper.key().get_existing_key(&id).await?;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<DeleteKeyResponse, Error> {
let helper = garage.locked_helper().await;
helper.delete_key(&mut key).await?;
let mut key = helper.key().get_existing_key(&self.id).await?;
Ok(Response::builder()
.status(StatusCode::NO_CONTENT)
.body(empty_body())?)
helper.delete_key(&mut key).await?;
Ok(DeleteKeyResponse)
}
}
async fn key_info_results(
garage: &Arc<Garage>,
key: Key,
show_secret: bool,
) -> Result<Response<ResBody>, Error> {
) -> Result<GetKeyInfoResponse, Error> {
let mut relevant_buckets = HashMap::new();
let key_state = key.state.as_option().unwrap();
@@ -192,8 +210,15 @@ async fn key_info_results(
}
}
let res = GetKeyInfoResult {
let res = GetKeyInfoResponse {
name: key_state.name.get().clone(),
created: key_state.created.map(|x| {
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
}),
expiration: key_state.expiration.get().map(|x| {
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
}),
expired: key_state.is_expired(now_msec()),
access_key_id: key.key_id.clone(),
secret_access_key: if show_secret {
Some(key_state.secret_key.clone())
@@ -207,7 +232,7 @@ async fn key_info_results(
.into_values()
.map(|bucket| {
let state = bucket.state.as_option().unwrap();
KeyInfoBucketResult {
KeyInfoBucketResponse {
id: hex::encode(bucket.id),
global_aliases: state
.aliases
@@ -237,43 +262,39 @@ async fn key_info_results(
.collect::<Vec<_>>(),
};
Ok(json_ok_response(&res)?)
Ok(res)
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetKeyInfoResult {
name: String,
access_key_id: String,
#[serde(skip_serializing_if = "is_default")]
secret_access_key: Option<String>,
permissions: KeyPerm,
buckets: Vec<KeyInfoBucketResult>,
}
fn apply_key_updates(key: &mut Key, updates: UpdateKeyRequestBody) -> Result<(), Error> {
if updates.never_expires && updates.expiration.is_some() {
return Err(Error::bad_request(
"cannot specify `expiration` and `never_expires`",
));
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct KeyPerm {
#[serde(default)]
create_bucket: bool,
}
let key_state = key.state.as_option_mut().unwrap();
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct KeyInfoBucketResult {
id: String,
global_aliases: Vec<String>,
local_aliases: Vec<String>,
permissions: ApiBucketKeyPerm,
}
if let Some(new_name) = updates.name {
key_state.name.update(new_name);
}
if let Some(expiration) = updates.expiration {
key_state
.expiration
.update(Some(expiration.timestamp_millis() as u64));
}
if updates.never_expires {
key_state.expiration.update(None);
}
if let Some(allow) = updates.allow {
if allow.create_bucket {
key_state.allow_create_bucket.update(true);
}
}
if let Some(deny) = updates.deny {
if deny.create_bucket {
key_state.allow_create_bucket.update(false);
}
}
#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ApiBucketKeyPerm {
#[serde(default)]
pub(crate) read: bool,
#[serde(default)]
pub(crate) write: bool,
#[serde(default)]
pub(crate) owner: bool,
Ok(())
}
+406
View File
@@ -0,0 +1,406 @@
use std::sync::Arc;
use garage_util::crdt::*;
use garage_util::data::*;
use garage_util::error::Error as GarageError;
use garage_rpc::layout;
use garage_model::garage::Garage;
use crate::api::*;
use crate::error::*;
use crate::{Admin, RequestHandler};
impl RequestHandler for GetClusterLayoutRequest {
type Response = GetClusterLayoutResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<GetClusterLayoutResponse, Error> {
Ok(format_cluster_layout(
garage.system.cluster_layout().inner(),
))
}
}
fn format_cluster_layout(layout: &layout::LayoutHistory) -> GetClusterLayoutResponse {
let current = layout.current();
let roles = current
.roles
.items()
.iter()
.filter_map(|(k, _, v)| v.0.clone().map(|x| (k, x)))
.map(|(k, v)| {
let stored_partitions = current.get_node_usage(k).ok().map(|x| x as u64);
LayoutNodeRole {
id: hex::encode(k),
zone: v.zone.clone(),
capacity: v.capacity,
stored_partitions,
usable_capacity: stored_partitions.map(|x| x * current.partition_size),
tags: v.tags.clone(),
}
})
.collect::<Vec<_>>();
let staged_role_changes = layout
.staging
.get()
.roles
.items()
.iter()
.filter(|(k, _, v)| current.roles.get(k) != Some(v))
.map(|(k, _, v)| match &v.0 {
None => NodeRoleChange {
id: hex::encode(k),
action: NodeRoleChangeEnum::Remove { remove: true },
},
Some(r) => NodeRoleChange {
id: hex::encode(k),
action: NodeRoleChangeEnum::Update(NodeAssignedRole {
zone: r.zone.clone(),
capacity: r.capacity,
tags: r.tags.clone(),
}),
},
})
.collect::<Vec<_>>();
let staged_parameters = if *layout.staging.get().parameters.get() != current.parameters {
Some((*layout.staging.get().parameters.get()).into())
} else {
None
};
GetClusterLayoutResponse {
version: current.version,
roles,
partition_size: current.partition_size,
parameters: current.parameters.into(),
staged_role_changes,
staged_parameters,
}
}
impl RequestHandler for GetClusterLayoutHistoryRequest {
type Response = GetClusterLayoutHistoryResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<GetClusterLayoutHistoryResponse, Error> {
let layout_helper = garage.system.cluster_layout();
let layout = layout_helper.inner();
let min_stored = layout.min_stored();
let versions = layout
.versions
.iter()
.rev()
.chain(layout.old_versions.iter().rev())
.map(|ver| {
let status = if ver.version == layout.current().version {
ClusterLayoutVersionStatus::Current
} else if ver.version >= min_stored {
ClusterLayoutVersionStatus::Draining
} else {
ClusterLayoutVersionStatus::Historical
};
ClusterLayoutVersion {
version: ver.version,
status,
storage_nodes: ver
.roles
.items()
.iter()
.filter(
|(_, _, x)| matches!(x, layout::NodeRoleV(Some(c)) if c.capacity.is_some()),
)
.count() as u64,
gateway_nodes: ver
.roles
.items()
.iter()
.filter(
|(_, _, x)| matches!(x, layout::NodeRoleV(Some(c)) if c.capacity.is_none()),
)
.count() as u64,
}
})
.collect::<Vec<_>>();
let all_nodes = layout.get_all_nodes();
let min_ack = layout_helper.ack_map_min();
let update_trackers = if layout.versions.len() > 1 {
Some(
all_nodes
.iter()
.map(|node| {
(
hex::encode(&node),
NodeUpdateTrackers {
ack: layout.update_trackers.ack_map.get(node, min_stored),
sync: layout.update_trackers.sync_map.get(node, min_stored),
sync_ack: layout.update_trackers.sync_ack_map.get(node, min_stored),
},
)
})
.collect(),
)
} else {
None
};
Ok(GetClusterLayoutHistoryResponse {
current_version: layout.current().version,
min_ack,
versions,
update_trackers,
})
}
}
// ----
// ---- update functions ----
impl RequestHandler for UpdateClusterLayoutRequest {
type Response = UpdateClusterLayoutResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<UpdateClusterLayoutResponse, Error> {
let mut layout = garage.system.cluster_layout().inner().clone();
let mut roles = layout.current().roles.clone();
roles.merge(&layout.staging.get().roles);
for change in self.roles {
let node = hex::decode(&change.id).ok_or_bad_request("Invalid node identifier")?;
let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?;
let new_role = match change.action {
NodeRoleChangeEnum::Remove { remove: true } => None,
NodeRoleChangeEnum::Update(NodeAssignedRole {
zone,
capacity,
tags,
}) => {
if matches!(capacity, Some(cap) if cap < 1024) {
return Err(Error::bad_request("Capacity should be at least 1K (1024)"));
}
Some(layout::NodeRole {
zone,
capacity,
tags,
})
}
_ => return Err(Error::bad_request("Invalid layout change")),
};
layout
.staging
.get_mut()
.roles
.merge(&roles.update_mutator(node, layout::NodeRoleV(new_role)));
}
if let Some(param) = self.parameters {
if let ZoneRedundancy::AtLeast(r_int) = param.zone_redundancy {
if r_int > layout.current().replication_factor {
return Err(Error::bad_request(format!(
"The zone redundancy must be smaller or equal to the replication factor ({}).",
layout.current().replication_factor
)));
} else if r_int < 1 {
return Err(Error::bad_request(
"The zone redundancy must be at least 1.",
));
}
}
layout.staging.get_mut().parameters.update(param.into());
}
garage
.system
.layout_manager
.update_cluster_layout(&layout)
.await?;
let res = format_cluster_layout(&layout);
Ok(UpdateClusterLayoutResponse(res))
}
}
impl RequestHandler for PreviewClusterLayoutChangesRequest {
type Response = PreviewClusterLayoutChangesResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<PreviewClusterLayoutChangesResponse, Error> {
let layout = garage.system.cluster_layout().inner().clone();
let new_ver = layout.current().version + 1;
match layout.apply_staged_changes(new_ver) {
Err(GarageError::Message(error)) => {
Ok(PreviewClusterLayoutChangesResponse::Error { error })
}
Err(e) => Err(e.into()),
Ok((new_layout, msg)) => Ok(PreviewClusterLayoutChangesResponse::Success {
message: msg,
new_layout: format_cluster_layout(&new_layout),
}),
}
}
}
impl RequestHandler for ApplyClusterLayoutRequest {
type Response = ApplyClusterLayoutResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<ApplyClusterLayoutResponse, Error> {
let layout = garage.system.cluster_layout().inner().clone();
let (layout, msg) = layout.apply_staged_changes(self.version)?;
garage
.system
.layout_manager
.update_cluster_layout(&layout)
.await?;
Ok(ApplyClusterLayoutResponse {
message: msg,
layout: format_cluster_layout(&layout),
})
}
}
impl RequestHandler for RevertClusterLayoutRequest {
type Response = RevertClusterLayoutResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<RevertClusterLayoutResponse, Error> {
let layout = garage.system.cluster_layout().inner().clone();
let layout = layout.revert_staged_changes()?;
garage
.system
.layout_manager
.update_cluster_layout(&layout)
.await?;
let res = format_cluster_layout(&layout);
Ok(RevertClusterLayoutResponse(res))
}
}
impl RequestHandler for ClusterLayoutSkipDeadNodesRequest {
type Response = ClusterLayoutSkipDeadNodesResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<ClusterLayoutSkipDeadNodesResponse, Error> {
let status = garage.system.get_known_nodes();
let mut layout = garage.system.cluster_layout().inner().clone();
let mut ack_updated = vec![];
let mut sync_updated = vec![];
if layout.versions.len() == 1 {
return Err(Error::bad_request(
"This command cannot be called when there is only one live cluster layout version",
));
}
let min_v = layout.min_stored();
if self.version <= min_v || self.version > layout.current().version {
return Err(Error::bad_request(format!(
"Invalid version, you may use the following version numbers: {}",
(min_v + 1..=layout.current().version)
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(" ")
)));
}
let all_nodes = layout.get_all_nodes();
for node in all_nodes.iter() {
// Update ACK tracker for dead nodes or for all nodes if --allow-missing-data
if self.allow_missing_data || !status.iter().any(|x| x.id == *node && x.is_up) {
if layout.update_trackers.ack_map.set_max(*node, self.version) {
ack_updated.push(hex::encode(node));
}
}
// If --allow-missing-data, update SYNC tracker for all nodes.
if self.allow_missing_data {
if layout.update_trackers.sync_map.set_max(*node, self.version) {
sync_updated.push(hex::encode(node));
}
}
}
garage
.system
.layout_manager
.update_cluster_layout(&layout)
.await?;
Ok(ClusterLayoutSkipDeadNodesResponse {
ack_updated,
sync_updated,
})
}
}
// ----
impl From<layout::ZoneRedundancy> for ZoneRedundancy {
fn from(x: layout::ZoneRedundancy) -> Self {
match x {
layout::ZoneRedundancy::Maximum => ZoneRedundancy::Maximum,
layout::ZoneRedundancy::AtLeast(x) => ZoneRedundancy::AtLeast(x),
}
}
}
impl Into<layout::ZoneRedundancy> for ZoneRedundancy {
fn into(self) -> layout::ZoneRedundancy {
match self {
ZoneRedundancy::Maximum => layout::ZoneRedundancy::Maximum,
ZoneRedundancy::AtLeast(x) => layout::ZoneRedundancy::AtLeast(x),
}
}
}
impl From<layout::LayoutParameters> for LayoutParameters {
fn from(x: layout::LayoutParameters) -> Self {
LayoutParameters {
zone_redundancy: x.zone_redundancy.into(),
}
}
}
impl Into<layout::LayoutParameters> for LayoutParameters {
fn into(self) -> layout::LayoutParameters {
layout::LayoutParameters {
zone_redundancy: self.zone_redundancy.into(),
}
}
}
+46
View File
@@ -0,0 +1,46 @@
#[macro_use]
extern crate tracing;
pub mod api_server;
mod error;
mod macros;
pub mod api;
pub mod openapi;
mod router_v0;
mod router_v1;
mod router_v2;
mod admin_token;
mod bucket;
mod cluster;
mod key;
mod layout;
mod special;
mod block;
mod node;
mod repair;
mod worker;
use std::sync::Arc;
use garage_model::garage::Garage;
pub use api_server::AdminApiServer as Admin;
pub enum Authorization {
None,
MetricsToken,
AdminToken,
}
pub trait RequestHandler {
type Response;
fn handle(
self,
garage: &Arc<Garage>,
admin: &Admin,
) -> impl std::future::Future<Output = Result<Self::Response, error::Error>> + Send;
}
+206
View File
@@ -0,0 +1,206 @@
macro_rules! admin_endpoints {
[
$(@special $special_endpoint:ident,)*
$($endpoint:ident,)*
] => {
paste! {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AdminApiRequest {
$(
$special_endpoint( [<$special_endpoint Request>] ),
)*
$(
$endpoint( [<$endpoint Request>] ),
)*
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum AdminApiResponse {
$(
$endpoint( [<$endpoint Response>] ),
)*
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TaggedAdminApiResponse {
$(
$endpoint( [<$endpoint Response>] ),
)*
}
impl AdminApiRequest {
pub fn name(&self) -> &'static str {
match self {
$(
Self::$special_endpoint(_) => stringify!($special_endpoint),
)*
$(
Self::$endpoint(_) => stringify!($endpoint),
)*
}
}
}
impl AdminApiResponse {
pub fn tagged(self) -> TaggedAdminApiResponse {
match self {
$(
Self::$endpoint(res) => TaggedAdminApiResponse::$endpoint(res),
)*
}
}
}
$(
impl From< [< $endpoint Request >] > for AdminApiRequest {
fn from(req: [< $endpoint Request >]) -> AdminApiRequest {
AdminApiRequest::$endpoint(req)
}
}
impl TryFrom<TaggedAdminApiResponse> for [< $endpoint Response >] {
type Error = TaggedAdminApiResponse;
fn try_from(resp: TaggedAdminApiResponse) -> Result< [< $endpoint Response >], TaggedAdminApiResponse> {
match resp {
TaggedAdminApiResponse::$endpoint(v) => Ok(v),
x => Err(x),
}
}
}
)*
impl RequestHandler for AdminApiRequest {
type Response = AdminApiResponse;
async fn handle(self, garage: &Arc<Garage>, admin: &Admin) -> Result<AdminApiResponse, Error> {
Ok(match self {
$(
AdminApiRequest::$special_endpoint(_) => panic!(
concat!(stringify!($special_endpoint), " needs to go through a special handler")
),
)*
$(
AdminApiRequest::$endpoint(req) => AdminApiResponse::$endpoint(req.handle(garage, admin).await?),
)*
})
}
}
}
};
}
macro_rules! local_admin_endpoints {
[
$($endpoint:ident,)*
] => {
paste! {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LocalAdminApiRequest {
$(
$endpoint( [<Local $endpoint Request>] ),
)*
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LocalAdminApiResponse {
$(
$endpoint( [<Local $endpoint Response>] ),
)*
}
$(
pub type [< $endpoint Request >] = MultiRequest< [< Local $endpoint Request >] >;
pub type [< $endpoint RequestBody >] = [< Local $endpoint Request >];
pub type [< $endpoint Response >] = MultiResponse< [< Local $endpoint Response >] >;
impl From< [< Local $endpoint Request >] > for LocalAdminApiRequest {
fn from(req: [< Local $endpoint Request >]) -> LocalAdminApiRequest {
LocalAdminApiRequest::$endpoint(req)
}
}
impl TryFrom<LocalAdminApiResponse> for [< Local $endpoint Response >] {
type Error = LocalAdminApiResponse;
fn try_from(resp: LocalAdminApiResponse) -> Result< [< Local $endpoint Response >], LocalAdminApiResponse> {
match resp {
LocalAdminApiResponse::$endpoint(v) => Ok(v),
x => Err(x),
}
}
}
impl RequestHandler for [< $endpoint Request >] {
type Response = [< $endpoint Response >];
async fn handle(self, garage: &Arc<Garage>, admin: &Admin) -> Result<Self::Response, Error> {
let to = find_matching_nodes(garage, self.node.as_str())?;
let resps = garage.system.rpc_helper().call_many(&admin.endpoint,
&to,
AdminRpc::Internal(self.body.into()),
RequestStrategy::with_priority(PRIO_NORMAL),
).await?;
let mut ret = [< $endpoint Response >] {
success: HashMap::new(),
error: HashMap::new(),
};
for (node, resp) in resps {
match resp {
Ok(AdminRpcResponse::InternalApiOkResponse(r)) => {
match [< Local $endpoint Response >]::try_from(r) {
Ok(r) => {
ret.success.insert(hex::encode(node), r);
}
Err(_) => {
ret.error.insert(hex::encode(node), "returned invalid value".to_string());
}
}
}
Ok(AdminRpcResponse::ApiErrorResponse{error_code, http_code, message}) => {
ret.error.insert(hex::encode(node), format!("{} ({}): {}", error_code, http_code, message));
}
Ok(_) => {
ret.error.insert(hex::encode(node), "returned invalid value".to_string());
}
Err(e) => {
ret.error.insert(hex::encode(node), e.to_string());
}
}
}
Ok(ret)
}
}
)*
impl LocalAdminApiRequest {
pub fn name(&self) -> &'static str {
match self {
$(
Self::$endpoint(_) => stringify!($endpoint),
)*
}
}
}
impl RequestHandler for LocalAdminApiRequest {
type Response = LocalAdminApiResponse;
async fn handle(self, garage: &Arc<Garage>, admin: &Admin) -> Result<LocalAdminApiResponse, Error> {
Ok(match self {
$(
LocalAdminApiRequest::$endpoint(req) => LocalAdminApiResponse::$endpoint(req.handle(garage, admin).await?),
)*
})
}
}
}
};
}
pub(crate) use admin_endpoints;
pub(crate) use local_admin_endpoints;
-8
View File
@@ -1,8 +0,0 @@
pub mod api_server;
mod error;
mod router_v0;
mod router_v1;
mod bucket;
mod cluster;
mod key;
+144
View File
@@ -0,0 +1,144 @@
use std::fmt::Write;
use std::sync::Arc;
use format_table::format_table_to_string;
use garage_util::error::Error as GarageError;
use garage_table::replication::*;
use garage_table::*;
use garage_model::garage::Garage;
use crate::api::*;
use crate::error::Error;
use crate::{Admin, RequestHandler};
impl RequestHandler for LocalGetNodeInfoRequest {
type Response = LocalGetNodeInfoResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<LocalGetNodeInfoResponse, Error> {
Ok(LocalGetNodeInfoResponse {
node_id: hex::encode(garage.system.id),
garage_version: garage_util::version::garage_version().to_string(),
garage_features: garage_util::version::garage_features()
.map(|features| features.iter().map(ToString::to_string).collect()),
rust_version: garage_util::version::rust_version().to_string(),
db_engine: garage.db.engine(),
})
}
}
impl RequestHandler for LocalCreateMetadataSnapshotRequest {
type Response = LocalCreateMetadataSnapshotResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<LocalCreateMetadataSnapshotResponse, Error> {
garage_model::snapshot::async_snapshot_metadata(garage).await?;
Ok(LocalCreateMetadataSnapshotResponse)
}
}
impl RequestHandler for LocalGetNodeStatisticsRequest {
type Response = LocalGetNodeStatisticsResponse;
// FIXME: return this as a JSON struct instead of text
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<LocalGetNodeStatisticsResponse, Error> {
let sys_status = garage.system.local_status();
let mut ret = format_table_to_string(vec![
format!("Node ID:\t{:?}", garage.system.id),
format!("Hostname:\t{}", sys_status.hostname.unwrap_or_default(),),
format!(
"Garage version:\t{}",
garage_util::version::garage_version(),
),
format!(
"Garage features:\t{}",
garage_util::version::garage_features()
.map(|list| list.join(", "))
.unwrap_or_else(|| "(unknown)".into()),
),
format!(
"Rust compiler version:\t{}",
garage_util::version::rust_version(),
),
format!("Database engine:\t{}", garage.db.engine()),
]);
// Gather table statistics
let mut table = vec![" Table\tItems\tMklItems\tMklTodo\tInsQueue\tGcTodo".into()];
table.push(gather_table_stats(&garage.admin_token_table)?);
table.push(gather_table_stats(&garage.bucket_table)?);
table.push(gather_table_stats(&garage.bucket_alias_table)?);
table.push(gather_table_stats(&garage.key_table)?);
table.push(gather_table_stats(&garage.object_table)?);
table.push(gather_table_stats(&garage.object_counter_table.table)?);
table.push(gather_table_stats(&garage.mpu_table)?);
table.push(gather_table_stats(&garage.mpu_counter_table.table)?);
table.push(gather_table_stats(&garage.version_table)?);
table.push(gather_table_stats(&garage.block_ref_table)?);
#[cfg(feature = "k2v")]
{
table.push(gather_table_stats(&garage.k2v.item_table)?);
table.push(gather_table_stats(&garage.k2v.counter_table.table)?);
}
write!(
&mut ret,
"\nTable stats:\n{}",
format_table_to_string(table)
)
.unwrap();
// Gather block manager statistics
writeln!(&mut ret, "\nBlock manager stats:").unwrap();
let rc_len = garage.block_manager.rc_len()?.to_string();
ret += &format_table_to_string(vec![
format!(" number of RC entries:\t{} (~= number of blocks)", rc_len),
format!(
" resync queue length:\t{}",
garage.block_manager.resync.queue_len()?
),
format!(
" blocks with resync errors:\t{}",
garage.block_manager.resync.errors_len()?
),
]);
Ok(LocalGetNodeStatisticsResponse { freeform: ret })
}
}
fn gather_table_stats<F, R>(t: &Arc<Table<F, R>>) -> Result<String, Error>
where
F: TableSchema + 'static,
R: TableReplication + 'static,
{
let data_len = t.data.store.len().map_err(GarageError::from)?.to_string();
let mkl_len = t.merkle_updater.merkle_tree_len()?.to_string();
Ok(format!(
" {}\t{}\t{}\t{}\t{}\t{}",
F::TABLE_NAME,
data_len,
mkl_len,
t.merkle_updater.todo_len()?,
t.data.insert_queue_len()?,
t.data.gc_todo_len()?
))
}
+924
View File
@@ -0,0 +1,924 @@
#![allow(dead_code)]
#![allow(non_snake_case)]
use utoipa::{Modify, OpenApi};
use crate::api::*;
// **********************************************
// Special endpoints
// **********************************************
#[utoipa::path(get,
path = "/metrics",
tag = "Special endpoints",
description = "Prometheus metrics endpoint",
security((), ("bearerAuth" = [])),
responses(
(status = 200, description = "Garage daemon metrics exported in Prometheus format"),
),
)]
fn Metrics() -> () {}
#[utoipa::path(get,
path = "/health",
tag = "Special endpoints",
description = "
Check cluster health. The status code returned by this function indicates
whether this Garage daemon can answer API requests.
Garage will return `200 OK` even if some storage nodes are disconnected,
as long as it is able to have a quorum of nodes for read and write operations.
",
security(()),
responses(
(status = 200, description = "Garage is able to answer requests"),
(status = 503, description = "This Garage daemon is not able to handle requests")
),
)]
fn Health() -> () {}
#[utoipa::path(get,
path = "/check",
tag = "Special endpoints",
description = "
Static website domain name check. Checks whether a bucket is configured to serve
a static website for the requested domain. This is used by reverse proxies such
as Caddy or Tricot, to avoid requesting TLS certificates for domain names that
do not correspond to an actual website.
",
params(
("domain", description = "The domain name to check for"),
),
security(()),
responses(
(status = 200, description = "The domain name redirects to a static website bucket"),
(status = 400, description = "No static website bucket exists for this domain")
),
)]
fn CheckDomain() -> () {}
// **********************************************
// Cluster operations
// **********************************************
#[utoipa::path(get,
path = "/v2/GetClusterStatus",
tag = "Cluster",
description = "
Returns the cluster's current status, including:
- ID of the node being queried and its version of the Garage daemon
- Live nodes
- Currently configured cluster layout
- Staged changes to the cluster layout
*Capacity is given in bytes*
",
responses(
(status = 200, description = "Cluster status report", body = GetClusterStatusResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetClusterStatus() -> () {}
#[utoipa::path(get,
path = "/v2/GetClusterHealth",
tag = "Cluster",
description = "Returns the global status of the cluster, the number of connected nodes (over the number of known ones), the number of healthy storage nodes (over the declared ones), and the number of healthy partitions (over the total).",
responses(
(status = 200, description = "Cluster health report", body = GetClusterHealthResponse),
),
)]
fn GetClusterHealth() -> () {}
#[utoipa::path(get,
path = "/v2/GetClusterStatistics",
tag = "Cluster",
description = "
Fetch global cluster statistics.
*Note: do not try to parse the `freeform` field of the response, it is given as a string specifically because its format is not stable.*
",
responses(
(status = 200, description = "Global cluster statistics", body = GetClusterStatisticsResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetClusterStatistics() -> () {}
#[utoipa::path(post,
path = "/v2/ConnectClusterNodes",
tag = "Cluster",
description = "Instructs this Garage node to connect to other Garage nodes at specified `<node_id>@<net_address>`. `node_id` is generated automatically on node start.",
request_body=ConnectClusterNodesRequest,
responses(
(status = 200, description = "The request has been handled correctly but it does not mean that all connection requests succeeded; some might have fail, you need to check the body!", body = ConnectClusterNodesResponse),
(status = 500, description = "Internal server error")
),
)]
fn ConnectClusterNodes() -> () {}
// **********************************************
// Admin API token operations
// **********************************************
#[utoipa::path(get,
path = "/v2/ListAdminTokens",
tag = "Admin API token",
description = "Returns all admin API tokens in the cluster.",
responses(
(status = 200, description = "Returns info about all admin API tokens", body = ListAdminTokensResponse),
(status = 500, description = "Internal server error")
),
)]
fn ListAdminTokens() -> () {}
#[utoipa::path(get,
path = "/v2/GetAdminTokenInfo",
tag = "Admin API token",
description = "
Return information about a specific admin API token.
You can search by specifying the exact token identifier (`id`) or by specifying a pattern (`search`).
",
params(GetAdminTokenInfoRequest),
responses(
(status = 200, description = "Information about the admin token", body = GetAdminTokenInfoResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetAdminTokenInfo() -> () {}
#[utoipa::path(post,
path = "/v2/CreateAdminToken",
tag = "Admin API token",
description = "Creates a new admin API token",
request_body = UpdateAdminTokenRequestBody,
responses(
(status = 200, description = "Admin token has been created", body = CreateAdminTokenResponse),
(status = 500, description = "Internal server error")
),
)]
fn CreateAdminToken() -> () {}
#[utoipa::path(post,
path = "/v2/UpdateAdminToken",
tag = "Admin API token",
description = "
Updates information about the specified admin API token.
",
request_body = UpdateAdminTokenRequestBody,
params(
("id", description = "Admin API token ID"),
),
responses(
(status = 200, description = "Admin token has been updated", body = UpdateAdminTokenResponse),
(status = 500, description = "Internal server error")
),
)]
fn UpdateAdminToken() -> () {}
#[utoipa::path(post,
path = "/v2/DeleteAdminToken",
tag = "Admin API token",
description = "Delete an admin API token from the cluster, revoking all its permissions.",
params(
("id", description = "Admin API token ID"),
),
responses(
(status = 200, description = "Admin token has been deleted"),
(status = 500, description = "Internal server error")
),
)]
fn DeleteAdminToken() -> () {}
// **********************************************
// Layout operations
// **********************************************
#[utoipa::path(get,
path = "/v2/GetClusterLayout",
tag = "Cluster layout",
description = "
Returns the cluster's current layout, including:
- Currently configured cluster layout
- Staged changes to the cluster layout
*Capacity is given in bytes*
",
responses(
(status = 200, description = "Current cluster layout", body = GetClusterLayoutResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetClusterLayout() -> () {}
#[utoipa::path(get,
path = "/v2/GetClusterLayoutHistory",
tag = "Cluster layout",
description = "
Returns the history of layouts in the cluster
",
responses(
(status = 200, description = "Cluster layout history", body = GetClusterLayoutHistoryResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetClusterLayoutHistory() -> () {}
#[utoipa::path(post,
path = "/v2/UpdateClusterLayout",
tag = "Cluster layout",
description = "
Send modifications to the cluster layout. These modifications will be included in the staged role changes, visible in subsequent calls of `GET /GetClusterHealth`. Once the set of staged changes is satisfactory, the user may call `POST /ApplyClusterLayout` to apply the changed changes, or `POST /RevertClusterLayout` to clear all of the staged changes in the layout.
Setting the capacity to `null` will configure the node as a gateway.
Otherwise, capacity must be now set in bytes (before Garage 0.9 it was arbitrary weights).
For example to declare 100GB, you must set `capacity: 100000000000`.
Garage uses internally the International System of Units (SI), it assumes that 1kB = 1000 bytes, and displays storage as kB, MB, GB (and not KiB, MiB, GiB that assume 1KiB = 1024 bytes).
",
request_body(
content=UpdateClusterLayoutRequest,
description="
To add a new node to the layout or to change the configuration of an existing node, simply set the values you want (`zone`, `capacity`, and `tags`).
To remove a node, simply pass the `remove: true` field.
This logic is represented in OpenAPI with a 'One Of' object.
Contrary to the CLI that may update only a subset of the fields capacity, zone and tags, when calling this API all of these values must be specified.
"
),
responses(
(status = 200, description = "Proposed changes have been added to the list of pending changes", body = UpdateClusterLayoutResponse),
(status = 500, description = "Internal server error")
),
)]
fn UpdateClusterLayout() -> () {}
#[utoipa::path(post,
path = "/v2/PreviewClusterLayoutChanges",
tag = "Cluster layout",
description = "
Computes a new layout taking into account the staged parameters, and returns it with detailed statistics. The new layout is not applied in the cluster.
*Note: do not try to parse the `message` field of the response, it is given as an array of string specifically because its format is not stable.*
",
responses(
(status = 200, description = "Information about the new layout", body = PreviewClusterLayoutChangesResponse),
(status = 500, description = "Internal server error")
),
)]
fn PreviewClusterLayoutChanges() -> () {}
#[utoipa::path(post,
path = "/v2/ApplyClusterLayout",
tag = "Cluster layout",
description = "
Applies to the cluster the layout changes currently registered as staged layout changes.
*Note: do not try to parse the `message` field of the response, it is given as an array of string specifically because its format is not stable.*
",
request_body=ApplyClusterLayoutRequest,
responses(
(status = 200, description = "The updated cluster layout has been applied in the cluster", body = ApplyClusterLayoutResponse),
(status = 500, description = "Internal server error")
),
)]
fn ApplyClusterLayout() -> () {}
#[utoipa::path(post,
path = "/v2/RevertClusterLayout",
tag = "Cluster layout",
description = "Clear staged layout changes",
responses(
(status = 200, description = "All pending changes to the cluster layout have been erased", body = RevertClusterLayoutResponse),
(status = 500, description = "Internal server error")
),
)]
fn RevertClusterLayout() -> () {}
#[utoipa::path(post,
path = "/v2/ClusterLayoutSkipDeadNodes",
tag = "Cluster layout",
description = "Force progress in layout update trackers",
request_body = ClusterLayoutSkipDeadNodesRequest,
responses(
(status = 200, description = "Request has been taken into account", body = ClusterLayoutSkipDeadNodesResponse),
(status = 500, description = "Internal server error")
),
)]
fn ClusterLayoutSkipDeadNodes() -> () {}
// **********************************************
// Access key operations
// **********************************************
#[utoipa::path(get,
path = "/v2/ListKeys",
tag = "Access key",
description = "Returns all API access keys in the cluster.",
responses(
(status = 200, description = "Returns the key identifier (aka `AWS_ACCESS_KEY_ID`) and its associated, human friendly, name if any (otherwise return an empty string)", body = ListKeysResponse),
(status = 500, description = "Internal server error")
),
)]
fn ListKeys() -> () {}
#[utoipa::path(get,
path = "/v2/GetKeyInfo",
tag = "Access key",
description = "
Return information about a specific key like its identifiers, its permissions and buckets on which it has permissions.
You can search by specifying the exact key identifier (`id`) or by specifying a pattern (`search`).
For confidentiality reasons, the secret key is not returned by default: you must pass the `showSecretKey` query parameter to get it.
",
params(GetKeyInfoRequest),
responses(
(status = 200, description = "Information about the access key", body = GetKeyInfoResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetKeyInfo() -> () {}
#[utoipa::path(post,
path = "/v2/CreateKey",
tag = "Access key",
description = "Creates a new API access key.",
request_body = CreateKeyRequest,
responses(
(status = 200, description = "Access key has been created", body = CreateKeyResponse),
(status = 500, description = "Internal server error")
),
)]
fn CreateKey() -> () {}
#[utoipa::path(post,
path = "/v2/ImportKey",
tag = "Access key",
description = "
Imports an existing API key. This feature must only be used for migrations and backup restore.
**Do not use it to generate custom key identifiers or you will break your Garage cluster.**
",
request_body = ImportKeyRequest,
responses(
(status = 200, description = "Access key has been imported", body = ImportKeyResponse),
(status = 500, description = "Internal server error")
),
)]
fn ImportKey() -> () {}
#[utoipa::path(post,
path = "/v2/UpdateKey",
tag = "Access key",
description = "
Updates information about the specified API access key.
*Note: the secret key is not returned in the response, `null` is sent instead.*
",
request_body = UpdateKeyRequestBody,
params(
("id", description = "Access key ID"),
),
responses(
(status = 200, description = "Access key has been updated", body = UpdateKeyResponse),
(status = 500, description = "Internal server error")
),
)]
fn UpdateKey() -> () {}
#[utoipa::path(post,
path = "/v2/DeleteKey",
tag = "Access key",
description = "Delete a key from the cluster. Its access will be removed from all the buckets. Buckets are not automatically deleted and can be dangling. You should manually delete them before. ",
params(
("id", description = "Access key ID"),
),
responses(
(status = 200, description = "Access key has been deleted"),
(status = 500, description = "Internal server error")
),
)]
fn DeleteKey() -> () {}
// **********************************************
// Bucket operations
// **********************************************
#[utoipa::path(get,
path = "/v2/ListBuckets",
tag = "Bucket",
description = "List all the buckets on the cluster with their UUID and their global and local aliases.",
responses(
(status = 200, description = "Returns the UUID of all the buckets and all their aliases", body = ListBucketsResponse),
(status = 500, description = "Internal server error")
),
)]
fn ListBuckets() -> () {}
#[utoipa::path(get,
path = "/v2/GetBucketInfo",
tag = "Bucket",
description = "
Given a bucket identifier (`id`) or a global alias (`alias`), get its information.
It includes its aliases, its web configuration, keys that have some permissions
on it, some statistics (number of objects, size), number of dangling multipart uploads,
and its quotas (if any).
",
params(GetBucketInfoRequest),
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = GetBucketInfoResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetBucketInfo() -> () {}
#[utoipa::path(post,
path = "/v2/CreateBucket",
tag = "Bucket",
description = "
Creates a new bucket, either with a global alias, a local one, or no alias at all.
Technically, you can also specify both `globalAlias` and `localAlias` and that would create two aliases.
",
request_body = CreateBucketRequest,
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = CreateBucketResponse),
(status = 500, description = "Internal server error")
),
)]
fn CreateBucket() -> () {}
#[utoipa::path(post,
path = "/v2/UpdateBucket",
tag = "Bucket",
description = "
All fields (`websiteAccess` and `quotas`) are optional.
If they are present, the corresponding modifications are applied to the bucket, otherwise nothing is changed.
In `websiteAccess`: if `enabled` is `true`, `indexDocument` must be specified.
The field `errorDocument` is optional, if no error document is set a generic
error message is displayed when errors happen. Conversely, if `enabled` is
`false`, neither `indexDocument` nor `errorDocument` must be specified.
In `quotas`: new values of `maxSize` and `maxObjects` must both be specified, or set to `null`
to remove the quotas. An absent value will be considered the same as a `null`. It is not possible
to change only one of the two quotas.
",
params(
("id", description = "ID of the bucket to update"),
),
request_body = UpdateBucketRequestBody,
responses(
(status = 200, description = "Bucket has been updated", body = UpdateBucketResponse),
(status = 404, description = "Bucket not found"),
(status = 500, description = "Internal server error")
),
)]
fn UpdateBucket() -> () {}
#[utoipa::path(post,
path = "/v2/DeleteBucket",
tag = "Bucket",
description = "
Deletes a storage bucket. A bucket cannot be deleted if it is not empty.
**Warning:** this will delete all aliases associated with the bucket!
",
params(
("id", description = "ID of the bucket to delete"),
),
responses(
(status = 200, description = "Bucket has been deleted"),
(status = 400, description = "Bucket is not empty"),
(status = 404, description = "Bucket not found"),
(status = 500, description = "Internal server error")
),
)]
fn DeleteBucket() -> () {}
#[utoipa::path(post,
path = "/v2/CleanupIncompleteUploads",
tag = "Bucket",
description = "Removes all incomplete multipart uploads that are older than the specified number of seconds.",
request_body = CleanupIncompleteUploadsRequest,
responses(
(status = 200, description = "The bucket was cleaned up successfully", body = CleanupIncompleteUploadsResponse),
(status = 500, description = "Internal server error")
),
)]
fn CleanupIncompleteUploads() -> () {}
#[utoipa::path(get,
path = "/v2/InspectObject",
tag = "Bucket",
description = "
Returns detailed information about an object in a bucket, including its internal state in Garage.
This API call can be used to list the data blocks referenced by an object,
as well as to view metadata associated to the object.
This call may return a list of more than one version for the object, for instance in the
case where there is a currently stored version of the object, and a newer version whose
upload is in progress and not yet finished.
",
params(InspectObjectRequest),
responses(
(status = 200, description = "Returns exhaustive information about the object", body = InspectObjectResponse),
(status = 404, description = "Object not found"),
(status = 500, description = "Internal server error")
),
)]
fn InspectObject() -> () {}
// **********************************************
// Operations on permissions for keys on buckets
// **********************************************
#[utoipa::path(post,
path = "/v2/AllowBucketKey",
tag = "Permission",
description = "
**DISCLAIMER**: Garage's developers are aware that this endpoint has an unconventional semantic. Be extra careful when implementing it, its behavior is not obvious.
Allows a key to do read/write/owner operations on a bucket.
Flags in permissions which have the value true will be activated. Other flags will remain unchanged (ie. they will keep their internal value).
For example, if you set read to true, the key will be allowed to read the bucket.
If you set it to false, the key will keeps its previous read permission.
If you want to disallow read for the key, check the DenyBucketKey operation.
",
request_body = AllowBucketKeyRequest,
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = AllowBucketKeyResponse),
(status = 500, description = "Internal server error")
),
)]
fn AllowBucketKey() -> () {}
#[utoipa::path(post,
path = "/v2/DenyBucketKey",
tag = "Permission",
description = "
**DISCLAIMER**: Garage's developers are aware that this endpoint has an unconventional semantic. Be extra careful when implementing it, its behavior is not obvious.
Denies a key from doing read/write/owner operations on a bucket.
Flags in permissions which have the value true will be deactivated. Other flags will remain unchanged.
For example, if you set read to true, the key will be denied from reading.
If you set read to false, the key will keep its previous permissions.
If you want the key to have the reading permission, check the AllowBucketKey operation.
",
request_body = DenyBucketKeyRequest,
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = DenyBucketKeyResponse),
(status = 500, description = "Internal server error")
),
)]
fn DenyBucketKey() -> () {}
// **********************************************
// Operations on bucket aliases
// **********************************************
#[utoipa::path(post,
path = "/v2/AddBucketAlias",
tag = "Bucket alias",
description = "Add an alias for the target bucket. This can be either a global or a local alias, depending on which fields are specified.",
request_body = AddBucketAliasRequest,
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = AddBucketAliasResponse),
(status = 500, description = "Internal server error")
),
)]
fn AddBucketAlias() -> () {}
#[utoipa::path(post,
path = "/v2/RemoveBucketAlias",
tag = "Bucket alias",
description = "Remove an alias for the target bucket. This can be either a global or a local alias, depending on which fields are specified.",
request_body = RemoveBucketAliasRequest,
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = RemoveBucketAliasResponse),
(status = 500, description = "Internal server error")
),
)]
fn RemoveBucketAlias() -> () {}
// **********************************************
// Node operations
// **********************************************
#[utoipa::path(get,
path = "/v2/GetNodeInfo",
tag = "Node",
description = "
Return information about the Garage daemon running on one or several nodes.
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalGetNodeInfoResponse>),
(status = 500, description = "Internal server error")
),
)]
fn GetNodeInfo() -> () {}
#[utoipa::path(get,
path = "/v2/GetNodeStatistics",
tag = "Node",
description = "
Fetch statistics for one or several Garage nodes.
*Note: do not try to parse the `freeform` field of the response, it is given as a string specifically because its format is not stable.*
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalGetNodeStatisticsResponse>),
(status = 500, description = "Internal server error")
),
)]
fn GetNodeStatistics() -> () {}
#[utoipa::path(post,
path = "/v2/CreateMetadataSnapshot",
tag = "Node",
description = "
Instruct one or several nodes to take a snapshot of their metadata databases.
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalCreateMetadataSnapshotResponse>),
(status = 500, description = "Internal server error")
),
)]
fn CreateMetadataSnapshot() -> () {}
#[utoipa::path(post,
path = "/v2/LaunchRepairOperation",
tag = "Node",
description = "
Launch a repair operation on one or several cluster nodes.
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
request_body = LocalLaunchRepairOperationRequest,
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalLaunchRepairOperationResponse>),
(status = 500, description = "Internal server error")
),
)]
fn LaunchRepairOperation() -> () {}
// **********************************************
// Worker operations
// **********************************************
#[utoipa::path(post,
path = "/v2/ListWorkers",
tag = "Worker",
description = "
List background workers currently running on one or several cluster nodes.
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
request_body = LocalListWorkersRequest,
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalListWorkersResponse>),
(status = 500, description = "Internal server error")
),
)]
fn ListWorkers() -> () {}
#[utoipa::path(post,
path = "/v2/GetWorkerInfo",
tag = "Worker",
description = "
Get information about the specified background worker on one or several cluster nodes.
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
request_body = LocalGetWorkerInfoRequest,
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalGetWorkerInfoResponse>),
(status = 500, description = "Internal server error")
),
)]
fn GetWorkerInfo() -> () {}
#[utoipa::path(post,
path = "/v2/GetWorkerVariable",
tag = "Worker",
description = "
Fetch values of one or several worker variables, from one or several cluster nodes.
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
request_body = LocalGetWorkerVariableRequest,
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalGetWorkerVariableResponse>),
(status = 500, description = "Internal server error")
),
)]
fn GetWorkerVariable() -> () {}
#[utoipa::path(post,
path = "/v2/SetWorkerVariable",
tag = "Worker",
description = "
Set the value for a worker variable, on one or several cluster nodes.
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
request_body = LocalSetWorkerVariableRequest,
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalSetWorkerVariableResponse>),
(status = 500, description = "Internal server error")
),
)]
fn SetWorkerVariable() -> () {}
// **********************************************
// Block operations
// **********************************************
#[utoipa::path(get,
path = "/v2/ListBlockErrors",
tag = "Block",
description = "
List data blocks that are currently in an errored state on one or several Garage nodes.
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalListBlockErrorsResponse>),
(status = 500, description = "Internal server error")
),
)]
fn ListBlockErrors() -> () {}
#[utoipa::path(post,
path = "/v2/GetBlockInfo",
tag = "Block",
description = "
Get detailed information about a data block stored on a Garage node, including all object versions and in-progress multipart uploads that contain a reference to this block.
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
request_body = LocalGetBlockInfoRequest,
responses(
(status = 200, description = "Detailed block information", body = MultiResponse<LocalGetBlockInfoResponse>),
(status = 500, description = "Internal server error")
),
)]
fn GetBlockInfo() -> () {}
#[utoipa::path(post,
path = "/v2/RetryBlockResync",
tag = "Block",
description = "
Instruct Garage node(s) to retry the resynchronization of one or several missing data block(s).
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
request_body = LocalRetryBlockResyncRequest,
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalRetryBlockResyncResponse>),
(status = 500, description = "Internal server error")
),
)]
fn RetryBlockResync() -> () {}
#[utoipa::path(post,
path = "/v2/PurgeBlocks",
tag = "Block",
description = "
Purge references to one or several missing data blocks.
This will remove all objects and in-progress multipart uploads that contain the specified data block(s). The objects will be permanently deleted from the buckets in which they appear. Use with caution.
",
params(
("node", description = "Node ID to query, or `*` for all nodes, or `self` for the node responding to the request"),
),
request_body = LocalPurgeBlocksRequest,
responses(
(status = 200, description = "Responses from individual cluster nodes", body = MultiResponse<LocalPurgeBlocksResponse>),
(status = 500, description = "Internal server error")
),
)]
fn PurgeBlocks() -> () {}
// **********************************************
// **********************************************
// **********************************************
struct SecurityAddon;
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
use utoipa::openapi::security::*;
let components = openapi.components.as_mut().unwrap(); // we can unwrap safely since there already is components registered.
components.add_security_scheme(
"bearerAuth",
SecurityScheme::Http(Http::builder().scheme(HttpAuthScheme::Bearer).build()),
)
}
}
#[derive(OpenApi)]
#[openapi(
info(
version = "v2.0.0",
title = "Garage administration API",
description = "Administrate your Garage cluster programatically, including status, layout, keys, buckets, and maintainance tasks.
*Disclaimer: This API may change in future Garage versions. Read the changelog and upgrade your scripts before upgrading. Additionnaly, this specification is early stage and can contain bugs, so be careful and please report any issues on our issue tracker.*",
contact(
name = "The Garage team",
email = "garagehq@deuxfleurs.fr",
url = "https://garagehq.deuxfleurs.fr/",
),
),
modifiers(&SecurityAddon),
security(("bearerAuth" = [])),
paths(
// Special ops
Metrics,
Health,
CheckDomain,
// Cluster operations
GetClusterHealth,
GetClusterStatus,
GetClusterStatistics,
ConnectClusterNodes,
// Admin token operations
ListAdminTokens,
GetAdminTokenInfo,
CreateAdminToken,
UpdateAdminToken,
DeleteAdminToken,
// Layout operations
GetClusterLayout,
GetClusterLayoutHistory,
UpdateClusterLayout,
PreviewClusterLayoutChanges,
ApplyClusterLayout,
RevertClusterLayout,
ClusterLayoutSkipDeadNodes,
// Key operations
ListKeys,
GetKeyInfo,
CreateKey,
ImportKey,
UpdateKey,
DeleteKey,
// Bucket operations
ListBuckets,
GetBucketInfo,
CreateBucket,
UpdateBucket,
DeleteBucket,
CleanupIncompleteUploads,
InspectObject,
// Operations on permissions
AllowBucketKey,
DenyBucketKey,
// Operations on aliases
AddBucketAlias,
RemoveBucketAlias,
// Node operations
GetNodeInfo,
GetNodeStatistics,
CreateMetadataSnapshot,
LaunchRepairOperation,
// Worker operations
ListWorkers,
GetWorkerInfo,
GetWorkerVariable,
SetWorkerVariable,
// Block operations
ListBlockErrors,
GetBlockInfo,
RetryBlockResync,
PurgeBlocks,
),
servers(
(url = "http://localhost:3903/", description = "A local server")
),
)]
pub struct ApiDoc;
+407
View File
@@ -0,0 +1,407 @@
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use tokio::sync::watch;
use garage_util::background::*;
use garage_util::data::*;
use garage_util::error::{Error as GarageError, OkOrMessage};
use garage_util::migrate::Migrate;
use garage_table::replication::*;
use garage_table::*;
use garage_block::manager::BlockManager;
use garage_block::repair::ScrubWorkerCommand;
use garage_model::garage::Garage;
use garage_model::s3::block_ref_table::*;
use garage_model::s3::mpu_table::*;
use garage_model::s3::object_table::*;
use garage_model::s3::version_table::*;
use crate::api::*;
use crate::error::Error;
use crate::{Admin, RequestHandler};
const RC_REPAIR_ITER_COUNT: usize = 64;
impl RequestHandler for LocalLaunchRepairOperationRequest {
type Response = LocalLaunchRepairOperationResponse;
async fn handle(
self,
garage: &Arc<Garage>,
admin: &Admin,
) -> Result<LocalLaunchRepairOperationResponse, Error> {
let bg = &admin.background;
match self.repair_type {
RepairType::Tables => {
info!("Launching a full sync of tables");
garage.bucket_table.syncer.add_full_sync()?;
garage.object_table.syncer.add_full_sync()?;
garage.version_table.syncer.add_full_sync()?;
garage.block_ref_table.syncer.add_full_sync()?;
garage.key_table.syncer.add_full_sync()?;
}
RepairType::Versions => {
info!("Repairing the versions table");
bg.spawn_worker(TableRepairWorker::new(garage.clone(), RepairVersions));
}
RepairType::MultipartUploads => {
info!("Repairing the multipart uploads table");
bg.spawn_worker(TableRepairWorker::new(garage.clone(), RepairMpu));
}
RepairType::BlockRefs => {
info!("Repairing the block refs table");
bg.spawn_worker(TableRepairWorker::new(garage.clone(), RepairBlockRefs));
}
RepairType::BlockRc => {
info!("Repairing the block reference counters");
bg.spawn_worker(BlockRcRepair::new(
garage.block_manager.clone(),
garage.block_ref_table.clone(),
));
}
RepairType::Blocks => {
info!("Repairing the stored blocks");
bg.spawn_worker(garage_block::repair::RepairWorker::new(
garage.block_manager.clone(),
));
}
RepairType::Scrub(cmd) => {
let cmd = match cmd {
ScrubCommand::Start => ScrubWorkerCommand::Start,
ScrubCommand::Pause => {
ScrubWorkerCommand::Pause(Duration::from_secs(3600 * 24))
}
ScrubCommand::Resume => ScrubWorkerCommand::Resume,
ScrubCommand::Cancel => ScrubWorkerCommand::Cancel,
};
info!("Sending command to scrub worker: {:?}", cmd);
garage.block_manager.send_scrub_command(cmd).await?;
}
RepairType::Rebalance => {
info!("Rebalancing the stored blocks among storage locations");
bg.spawn_worker(garage_block::repair::RebalanceWorker::new(
garage.block_manager.clone(),
));
}
RepairType::Aliases => {
info!("Repairing bucket aliases (foreground)");
garage.locked_helper().await.repair_aliases().await?;
}
}
Ok(LocalLaunchRepairOperationResponse)
}
}
// ----
trait TableRepair: Send + Sync + 'static {
type T: TableSchema;
fn table(garage: &Garage) -> &Table<Self::T, TableShardedReplication>;
fn process(
&mut self,
garage: &Garage,
entry: <<Self as TableRepair>::T as TableSchema>::E,
) -> impl Future<Output = Result<bool, GarageError>> + Send;
}
struct TableRepairWorker<T: TableRepair> {
garage: Arc<Garage>,
pos: Vec<u8>,
counter: usize,
repairs: usize,
inner: T,
}
impl<R: TableRepair> TableRepairWorker<R> {
fn new(garage: Arc<Garage>, inner: R) -> Self {
Self {
garage,
inner,
pos: vec![],
counter: 0,
repairs: 0,
}
}
}
#[async_trait]
impl<R: TableRepair> Worker for TableRepairWorker<R> {
fn name(&self) -> String {
format!("{} repair worker", R::T::TABLE_NAME)
}
fn status(&self) -> WorkerStatus {
WorkerStatus {
progress: Some(format!("{} ({})", self.counter, self.repairs)),
..Default::default()
}
}
async fn work(
&mut self,
_must_exit: &mut watch::Receiver<bool>,
) -> Result<WorkerState, GarageError> {
let (item_bytes, next_pos) = match R::table(&self.garage).data.store.get_gt(&self.pos)? {
Some((k, v)) => (v, k),
None => {
info!(
"{}: finished, done {}, fixed {}",
self.name(),
self.counter,
self.repairs
);
return Ok(WorkerState::Done);
}
};
let entry = <R::T as TableSchema>::E::decode(&item_bytes)
.ok_or_message("Cannot decode table entry")?;
if self.inner.process(&self.garage, entry).await? {
self.repairs += 1;
}
self.counter += 1;
self.pos = next_pos;
Ok(WorkerState::Busy)
}
async fn wait_for_work(&mut self) -> WorkerState {
unreachable!()
}
}
// ----
struct RepairVersions;
impl TableRepair for RepairVersions {
type T = VersionTable;
fn table(garage: &Garage) -> &Table<Self::T, TableShardedReplication> {
&garage.version_table
}
async fn process(&mut self, garage: &Garage, version: Version) -> Result<bool, GarageError> {
if !version.deleted.get() {
let ref_exists = match &version.backlink {
VersionBacklink::Object { bucket_id, key } => garage
.object_table
.get(bucket_id, key)
.await?
.map(|o| {
o.versions().iter().any(|x| {
x.uuid == version.uuid && x.state != ObjectVersionState::Aborted
})
})
.unwrap_or(false),
VersionBacklink::MultipartUpload { upload_id } => garage
.mpu_table
.get(upload_id, &EmptyKey)
.await?
.map(|u| !u.deleted.get())
.unwrap_or(false),
};
if !ref_exists {
info!("Repair versions: marking version as deleted: {:?}", version);
garage
.version_table
.insert(&Version::new(version.uuid, version.backlink, true))
.await?;
return Ok(true);
}
}
Ok(false)
}
}
// ----
struct RepairBlockRefs;
impl TableRepair for RepairBlockRefs {
type T = BlockRefTable;
fn table(garage: &Garage) -> &Table<Self::T, TableShardedReplication> {
&garage.block_ref_table
}
async fn process(
&mut self,
garage: &Garage,
mut block_ref: BlockRef,
) -> Result<bool, GarageError> {
if !block_ref.deleted.get() {
let ref_exists = garage
.version_table
.get(&block_ref.version, &EmptyKey)
.await?
.map(|v| !v.deleted.get())
.unwrap_or(false);
if !ref_exists {
info!(
"Repair block ref: marking block_ref as deleted: {:?}",
block_ref
);
block_ref.deleted.set();
garage.block_ref_table.insert(&block_ref).await?;
return Ok(true);
}
}
Ok(false)
}
}
// ----
struct RepairMpu;
impl TableRepair for RepairMpu {
type T = MultipartUploadTable;
fn table(garage: &Garage) -> &Table<Self::T, TableShardedReplication> {
&garage.mpu_table
}
async fn process(
&mut self,
garage: &Garage,
mut mpu: MultipartUpload,
) -> Result<bool, GarageError> {
if !mpu.deleted.get() {
let ref_exists = garage
.object_table
.get(&mpu.bucket_id, &mpu.key)
.await?
.map(|o| {
o.versions()
.iter()
.any(|x| x.uuid == mpu.upload_id && x.is_uploading(Some(true)))
})
.unwrap_or(false);
if !ref_exists {
info!(
"Repair multipart uploads: marking mpu as deleted: {:?}",
mpu
);
mpu.parts.clear();
mpu.deleted.set();
garage.mpu_table.insert(&mpu).await?;
return Ok(true);
}
}
Ok(false)
}
}
// ===== block reference counter repair =====
pub struct BlockRcRepair {
block_manager: Arc<BlockManager>,
block_ref_table: Arc<Table<BlockRefTable, TableShardedReplication>>,
cursor: Hash,
counter: u64,
repairs: u64,
}
impl BlockRcRepair {
fn new(
block_manager: Arc<BlockManager>,
block_ref_table: Arc<Table<BlockRefTable, TableShardedReplication>>,
) -> Self {
Self {
block_manager,
block_ref_table,
cursor: [0u8; 32].into(),
counter: 0,
repairs: 0,
}
}
}
#[async_trait]
impl Worker for BlockRcRepair {
fn name(&self) -> String {
format!("Block refcount repair worker")
}
fn status(&self) -> WorkerStatus {
WorkerStatus {
progress: Some(format!("{} ({})", self.counter, self.repairs)),
..Default::default()
}
}
async fn work(
&mut self,
_must_exit: &mut watch::Receiver<bool>,
) -> Result<WorkerState, GarageError> {
for _i in 0..RC_REPAIR_ITER_COUNT {
let next1 = self
.block_manager
.rc
.rc_table
.range(self.cursor.as_slice()..)?
.next()
.transpose()?
.map(|(k, _)| Hash::try_from(k.as_slice()).unwrap());
let next2 = self
.block_ref_table
.data
.store
.range(self.cursor.as_slice()..)?
.next()
.transpose()?
.map(|(k, _)| Hash::try_from(&k[..32]).unwrap());
let next = match (next1, next2) {
(Some(k1), Some(k2)) => std::cmp::min(k1, k2),
(Some(k), None) | (None, Some(k)) => k,
(None, None) => {
info!(
"{}: finished, done {}, fixed {}",
self.name(),
self.counter,
self.repairs
);
return Ok(WorkerState::Done);
}
};
if self.block_manager.rc.recalculate_rc(&next)?.1 {
self.repairs += 1;
}
self.counter += 1;
if let Some(next_incr) = next.increment() {
self.cursor = next_incr;
} else {
info!(
"{}: finished, done {}, fixed {}",
self.name(),
self.counter,
self.repairs
);
return Ok(WorkerState::Done);
}
}
Ok(WorkerState::Busy)
}
async fn wait_for_work(&mut self) -> WorkerState {
unreachable!()
}
}
+3 -2
View File
@@ -2,8 +2,9 @@ use std::borrow::Cow;
use hyper::{Method, Request};
use crate::admin::error::*;
use crate::router_macros::*;
use garage_api_common::router_macros::*;
use crate::error::*;
router_match! {@func
+3 -17
View File
@@ -2,15 +2,10 @@ use std::borrow::Cow;
use hyper::{Method, Request};
use crate::admin::error::*;
use crate::admin::router_v0;
use crate::router_macros::*;
use garage_api_common::router_macros::*;
pub enum Authorization {
None,
MetricsToken,
AdminToken,
}
use crate::error::*;
use crate::router_v0;
router_match! {@func
@@ -210,15 +205,6 @@ impl Endpoint {
))),
}
}
/// Get the kind of authorization which is required to perform the operation.
pub fn authorization_type(&self) -> Authorization {
match self {
Self::Health => Authorization::None,
Self::CheckDomain => Authorization::None,
Self::Metrics => Authorization::MetricsToken,
_ => Authorization::AdminToken,
}
}
}
generateQueryParameters! {
+275
View File
@@ -0,0 +1,275 @@
use std::borrow::Cow;
use hyper::body::Incoming as IncomingBody;
use hyper::{Method, Request};
use paste::paste;
use garage_api_common::helpers::*;
use garage_api_common::router_macros::*;
use crate::api::*;
use crate::error::*;
use crate::router_v1;
use crate::Authorization;
impl AdminApiRequest {
/// Determine which S3 endpoint a request is for using the request, and a bucket which was
/// possibly extracted from the Host header.
/// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets
pub async fn from_request(req: Request<IncomingBody>) -> Result<Self, Error> {
let uri = req.uri().clone();
let path = uri.path();
let query = uri.query();
let method = req.method().clone();
let mut query = QueryParameters::from_query(query.unwrap_or_default())?;
let res = router_match!(@gen_path_parser_v2 (&method, path, "/v2/", query, req) [
@special OPTIONS _ => Options (),
@special GET "/check" => CheckDomain (query::domain),
@special GET "/health" => Health (),
@special GET "/metrics" => Metrics (),
// Cluster endpoints
GET GetClusterStatus (),
GET GetClusterHealth (),
POST ConnectClusterNodes (body),
// Admin token endpoints
GET ListAdminTokens (),
GET GetAdminTokenInfo (query_opt::id, query_opt::search),
POST CreateAdminToken (body),
POST UpdateAdminToken (body_field, query::id),
POST DeleteAdminToken (query::id),
// Layout endpoints
GET GetClusterLayout (),
GET GetClusterLayoutHistory (),
POST UpdateClusterLayout (body),
POST PreviewClusterLayoutChanges (),
POST ApplyClusterLayout (body),
POST RevertClusterLayout (),
POST ClusterLayoutSkipDeadNodes (body),
// API key endpoints
GET GetKeyInfo (query_opt::id, query_opt::search, parse_default(false)::show_secret_key),
POST UpdateKey (body_field, query::id),
POST CreateKey (body),
POST ImportKey (body),
POST DeleteKey (query::id),
GET ListKeys (),
// Bucket endpoints
GET GetBucketInfo (query_opt::id, query_opt::global_alias, query_opt::search),
GET ListBuckets (),
POST CreateBucket (body),
POST DeleteBucket (query::id),
POST UpdateBucket (body_field, query::id),
POST CleanupIncompleteUploads (body),
GET InspectObject (query::bucket_id, query::key),
// Bucket-key permissions
POST AllowBucketKey (body),
POST DenyBucketKey (body),
// Bucket aliases
POST AddBucketAlias (body),
POST RemoveBucketAlias (body),
// Node APIs
GET GetNodeInfo (default::body, query::node),
POST CreateMetadataSnapshot (default::body, query::node),
GET GetNodeStatistics (default::body, query::node),
GET GetClusterStatistics (),
POST LaunchRepairOperation (body_field, query::node),
// Worker APIs
POST ListWorkers (body_field, query::node),
POST GetWorkerInfo (body_field, query::node),
POST GetWorkerVariable (body_field, query::node),
POST SetWorkerVariable (body_field, query::node),
// Block APIs
GET ListBlockErrors (default::body, query::node),
POST GetBlockInfo (body_field, query::node),
POST RetryBlockResync (body_field, query::node),
POST PurgeBlocks (body_field, query::node),
]);
if let Some(message) = query.nonempty_message() {
debug!("Unused query parameter: {}", message)
}
Ok(res)
}
/// Some endpoints work exactly the same in their v2/ version as they did in their v1/ version.
/// For these endpoints, we can convert a v1/ call to its equivalent as if it was made using
/// its v2/ URL.
pub async fn from_v1(
v1_endpoint: router_v1::Endpoint,
req: Request<IncomingBody>,
) -> Result<Self, Error> {
use router_v1::Endpoint;
match v1_endpoint {
// GetClusterStatus semantics changed:
// info about local node is no longer returned
Endpoint::GetClusterHealth => {
Ok(AdminApiRequest::GetClusterHealth(GetClusterHealthRequest))
}
Endpoint::ConnectClusterNodes => {
let req = parse_json_body::<ConnectClusterNodesRequest, _, Error>(req).await?;
Ok(AdminApiRequest::ConnectClusterNodes(req))
}
// Layout
Endpoint::GetClusterLayout => {
Ok(AdminApiRequest::GetClusterLayout(GetClusterLayoutRequest))
}
// UpdateClusterLayout semantics changed
Endpoint::ApplyClusterLayout => {
let param = parse_json_body::<ApplyClusterLayoutRequest, _, Error>(req).await?;
Ok(AdminApiRequest::ApplyClusterLayout(param))
}
Endpoint::RevertClusterLayout => Ok(AdminApiRequest::RevertClusterLayout(
RevertClusterLayoutRequest,
)),
// Keys
Endpoint::ListKeys => Ok(AdminApiRequest::ListKeys(ListKeysRequest)),
Endpoint::GetKeyInfo {
id,
search,
show_secret_key,
} => {
let show_secret_key = show_secret_key.map(|x| x == "true").unwrap_or(false);
Ok(AdminApiRequest::GetKeyInfo(GetKeyInfoRequest {
id,
search,
show_secret_key,
}))
}
Endpoint::CreateKey => {
let req = parse_json_body::<CreateKeyRequest, _, Error>(req).await?;
Ok(AdminApiRequest::CreateKey(req))
}
Endpoint::ImportKey => {
let req = parse_json_body::<ImportKeyRequest, _, Error>(req).await?;
Ok(AdminApiRequest::ImportKey(req))
}
Endpoint::UpdateKey { id } => {
let body = parse_json_body::<UpdateKeyRequestBody, _, Error>(req).await?;
Ok(AdminApiRequest::UpdateKey(UpdateKeyRequest { id, body }))
}
// DeleteKey semantics changed:
// - in v1/ : HTTP DELETE => HTTP 204 No Content
// - in v2/ : HTTP POST => HTTP 200 Ok
// Endpoint::DeleteKey { id } => Ok(AdminApiRequest::DeleteKey(DeleteKeyRequest { id })),
// Buckets
Endpoint::ListBuckets => Ok(AdminApiRequest::ListBuckets(ListBucketsRequest)),
Endpoint::GetBucketInfo { id, global_alias } => {
Ok(AdminApiRequest::GetBucketInfo(GetBucketInfoRequest {
id,
global_alias,
search: None,
}))
}
Endpoint::CreateBucket => {
let req = parse_json_body::<CreateBucketRequest, _, Error>(req).await?;
Ok(AdminApiRequest::CreateBucket(req))
}
// DeleteBucket semantics changed::
// - in v1/ : HTTP DELETE => HTTP 204 No Content
// - in v2/ : HTTP POST => HTTP 200 Ok
// Endpoint::DeleteBucket { id } => {
// Ok(AdminApiRequest::DeleteBucket(DeleteBucketRequest { id }))
// }
Endpoint::UpdateBucket { id } => {
let body = parse_json_body::<UpdateBucketRequestBody, _, Error>(req).await?;
Ok(AdminApiRequest::UpdateBucket(UpdateBucketRequest {
id,
body,
}))
}
// Bucket-key permissions
Endpoint::BucketAllowKey => {
let req = parse_json_body::<BucketKeyPermChangeRequest, _, Error>(req).await?;
Ok(AdminApiRequest::AllowBucketKey(AllowBucketKeyRequest(req)))
}
Endpoint::BucketDenyKey => {
let req = parse_json_body::<BucketKeyPermChangeRequest, _, Error>(req).await?;
Ok(AdminApiRequest::DenyBucketKey(DenyBucketKeyRequest(req)))
}
// Bucket aliasing
Endpoint::GlobalAliasBucket { id, alias } => {
Ok(AdminApiRequest::AddBucketAlias(AddBucketAliasRequest {
bucket_id: id,
alias: BucketAliasEnum::Global {
global_alias: alias,
},
}))
}
Endpoint::GlobalUnaliasBucket { id, alias } => Ok(AdminApiRequest::RemoveBucketAlias(
RemoveBucketAliasRequest {
bucket_id: id,
alias: BucketAliasEnum::Global {
global_alias: alias,
},
},
)),
Endpoint::LocalAliasBucket {
id,
access_key_id,
alias,
} => Ok(AdminApiRequest::AddBucketAlias(AddBucketAliasRequest {
bucket_id: id,
alias: BucketAliasEnum::Local {
local_alias: alias,
access_key_id,
},
})),
Endpoint::LocalUnaliasBucket {
id,
access_key_id,
alias,
} => Ok(AdminApiRequest::RemoveBucketAlias(
RemoveBucketAliasRequest {
bucket_id: id,
alias: BucketAliasEnum::Local {
local_alias: alias,
access_key_id,
},
},
)),
// For endpoints that have different body content syntax, issue
// deprecation warning
_ => Err(Error::bad_request(format!(
"v1/ endpoint is no longer supported: {}",
v1_endpoint.name()
))),
}
}
/// Get the kind of authorization which is required to perform the operation.
pub fn authorization_type(&self) -> Authorization {
match self {
Self::Options(_) | Self::Health(_) | Self::CheckDomain(_) => Authorization::None,
Self::Metrics(_) => Authorization::MetricsToken,
_ => Authorization::AdminToken,
}
}
}
generateQueryParameters! {
keywords: [],
fields: [
"node" => node,
"domain" => domain,
"format" => format,
"id" => id,
"search" => search,
"globalAlias" => global_alias,
"alias" => alias,
"accessKeyId" => access_key_id,
"showSecretKey" => show_secret_key,
"bucketId" => bucket_id,
"key" => key
]
}
+173
View File
@@ -0,0 +1,173 @@
use std::sync::Arc;
use http::header::{
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW,
};
use hyper::{Response, StatusCode};
#[cfg(feature = "metrics")]
use prometheus::{Encoder, TextEncoder};
use garage_model::garage::Garage;
use garage_rpc::system::ClusterHealthStatus;
use garage_api_common::helpers::*;
use crate::api::{CheckDomainRequest, HealthRequest, MetricsRequest, OptionsRequest};
use crate::api_server::ResBody;
use crate::error::*;
use crate::{Admin, RequestHandler};
impl RequestHandler for OptionsRequest {
type Response = Response<ResBody>;
async fn handle(
self,
_garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<Response<ResBody>, Error> {
Ok(Response::builder()
.status(StatusCode::OK)
.header(ALLOW, "OPTIONS,GET,POST")
.header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS,GET,POST")
.header(ACCESS_CONTROL_ALLOW_HEADERS, "authorization,content-type")
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(empty_body())?)
}
}
impl RequestHandler for MetricsRequest {
type Response = Response<ResBody>;
async fn handle(
self,
_garage: &Arc<Garage>,
admin: &Admin,
) -> Result<Response<ResBody>, Error> {
#[cfg(feature = "metrics")]
{
use opentelemetry::trace::Tracer;
let mut buffer = vec![];
let encoder = TextEncoder::new();
let tracer = opentelemetry::global::tracer("garage");
let metric_families = tracer.in_span("admin/gather_metrics", |_| {
admin.exporter.registry().gather()
});
encoder
.encode(&metric_families, &mut buffer)
.ok_or_internal_error("Could not serialize metrics")?;
Ok(Response::builder()
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, encoder.format_type())
.body(bytes_body(buffer.into()))?)
}
#[cfg(not(feature = "metrics"))]
Err(Error::bad_request(
"Garage was built without the metrics feature".to_string(),
))
}
}
impl RequestHandler for HealthRequest {
type Response = Response<ResBody>;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<Response<ResBody>, Error> {
let health = garage.system.health();
let (status, status_str) = match health.status {
ClusterHealthStatus::Healthy => (StatusCode::OK, "Garage is fully operational"),
ClusterHealthStatus::Degraded => (
StatusCode::OK,
"Garage is operational but some storage nodes are unavailable",
),
ClusterHealthStatus::Unavailable => (
StatusCode::SERVICE_UNAVAILABLE,
"Quorum is not available for some/all partitions, reads and writes will fail",
),
};
let status_str = format!(
"{}\nConsult the full health check API endpoint at /v2/GetClusterHealth for more details\n",
status_str
);
Ok(Response::builder()
.status(status)
.header(http::header::CONTENT_TYPE, "text/plain")
.body(string_body(status_str))?)
}
}
impl RequestHandler for CheckDomainRequest {
type Response = Response<ResBody>;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<Response<ResBody>, Error> {
if check_domain(garage, &self.domain).await? {
Ok(Response::builder()
.status(StatusCode::OK)
.body(string_body(format!(
"Domain '{}' is managed by Garage",
self.domain
)))?)
} else {
Err(Error::bad_request(format!(
"Domain '{}' is not managed by Garage",
self.domain
)))
}
}
}
async fn check_domain(garage: &Arc<Garage>, domain: &str) -> Result<bool, Error> {
// Resolve bucket from domain name, inferring if the website must be activated for the
// domain to be valid.
let (bucket_name, must_check_website) = if let Some(bname) = garage
.config
.s3_api
.root_domain
.as_ref()
.and_then(|rd| host_to_bucket(domain, rd))
{
(bname.to_string(), false)
} else if let Some(bname) = garage
.config
.s3_web
.as_ref()
.and_then(|sw| host_to_bucket(domain, sw.root_domain.as_str()))
{
(bname.to_string(), true)
} else {
(domain.to_string(), true)
};
let bucket = match garage
.bucket_helper()
.resolve_global_bucket_fast(&bucket_name)?
{
Some(b) => b,
None => return Ok(false),
};
if !must_check_website {
return Ok(true);
}
let bucket_state = bucket.state.as_option().unwrap();
let bucket_website_config = bucket_state.website_config.get();
match bucket_website_config {
Some(_v) => Ok(true),
None => Ok(false),
}
}
+118
View File
@@ -0,0 +1,118 @@
use std::collections::HashMap;
use std::sync::Arc;
use garage_util::background::*;
use garage_util::time::now_msec;
use garage_model::garage::Garage;
use crate::api::*;
use crate::error::Error;
use crate::{Admin, RequestHandler};
impl RequestHandler for LocalListWorkersRequest {
type Response = LocalListWorkersResponse;
async fn handle(
self,
_garage: &Arc<Garage>,
admin: &Admin,
) -> Result<LocalListWorkersResponse, Error> {
let workers = admin.background.get_worker_info();
let info = workers
.into_iter()
.filter(|(_, w)| {
(!self.busy_only
|| matches!(w.state, WorkerState::Busy | WorkerState::Throttled(_)))
&& (!self.error_only || w.errors > 0)
})
.map(|(id, w)| worker_info_to_api(id as u64, w))
.collect::<Vec<_>>();
Ok(LocalListWorkersResponse(info))
}
}
impl RequestHandler for LocalGetWorkerInfoRequest {
type Response = LocalGetWorkerInfoResponse;
async fn handle(
self,
_garage: &Arc<Garage>,
admin: &Admin,
) -> Result<LocalGetWorkerInfoResponse, Error> {
let info = admin
.background
.get_worker_info()
.get(&(self.id as usize))
.ok_or(Error::NoSuchWorker(self.id))?
.clone();
Ok(LocalGetWorkerInfoResponse(worker_info_to_api(
self.id, info,
)))
}
}
impl RequestHandler for LocalGetWorkerVariableRequest {
type Response = LocalGetWorkerVariableResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<LocalGetWorkerVariableResponse, Error> {
let mut res = HashMap::new();
if let Some(k) = self.variable {
res.insert(k.clone(), garage.bg_vars.get(&k)?);
} else {
let vars = garage.bg_vars.get_all();
for (k, v) in vars.iter() {
res.insert(k.to_string(), v.to_string());
}
}
Ok(LocalGetWorkerVariableResponse(res))
}
}
impl RequestHandler for LocalSetWorkerVariableRequest {
type Response = LocalSetWorkerVariableResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<LocalSetWorkerVariableResponse, Error> {
garage.bg_vars.set(&self.variable, &self.value)?;
Ok(LocalSetWorkerVariableResponse {
variable: self.variable,
value: self.value,
})
}
}
// ---- helper functions ----
fn worker_info_to_api(id: u64, info: WorkerInfo) -> WorkerInfoResp {
WorkerInfoResp {
id,
name: info.name,
state: match info.state {
WorkerState::Busy => WorkerStateResp::Busy,
WorkerState::Throttled(t) => WorkerStateResp::Throttled { duration_secs: t },
WorkerState::Idle => WorkerStateResp::Idle,
WorkerState::Done => WorkerStateResp::Done,
},
errors: info.errors as u64,
consecutive_errors: info.consecutive_errors as u64,
last_error: info.last_error.map(|(message, t)| WorkerLastError {
message,
secs_ago: now_msec().saturating_sub(t) / 1000,
}),
tranquility: info.status.tranquility,
progress: info.status.progress,
queue_length: info.status.queue_length,
persistent_errors: info.status.persistent_errors,
freeform: info.status.freeform,
}
}
+47
View File
@@ -0,0 +1,47 @@
[package]
name = "garage_api_common"
version = "2.0.0"
authors = ["Alex Auvolat <alex@adnab.me>"]
edition = "2018"
license = "AGPL-3.0"
description = "Common functions for the API server crates for the Garage object store"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
readme = "../../../README.md"
[lib]
path = "lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
garage_model.workspace = true
garage_table.workspace = true
garage_util.workspace = true
base64.workspace = true
bytes.workspace = true
chrono.workspace = true
crc-fast.workspace = true
crypto-common.workspace = true
err-derive.workspace = true
hex.workspace = true
hmac.workspace = true
md-5.workspace = true
tracing.workspace = true
nom.workspace = true
pin-project.workspace = true
sha1.workspace = true
sha2.workspace = true
futures.workspace = true
tokio.workspace = true
http.workspace = true
http-body-util.workspace = true
hyper = { workspace = true, default-features = false, features = ["server", "http1"] }
hyper-util.workspace = true
url.workspace = true
serde.workspace = true
serde_json.workspace = true
opentelemetry.workspace = true
@@ -1,3 +1,5 @@
use std::convert::TryFrom;
use err_derive::Error;
use hyper::StatusCode;
@@ -55,13 +57,40 @@ pub enum CommonError {
InvalidBucketName(String),
}
#[macro_export]
macro_rules! commonErrorDerivative {
( $error_struct: ident ) => {
impl From<garage_util::error::Error> for $error_struct {
fn from(err: garage_util::error::Error) -> Self {
Self::Common(CommonError::InternalError(err))
}
}
impl From<http::Error> for $error_struct {
fn from(err: http::Error) -> Self {
Self::Common(CommonError::Http(err))
}
}
impl From<hyper::Error> for $error_struct {
fn from(err: hyper::Error) -> Self {
Self::Common(CommonError::Hyper(err))
}
}
impl From<hyper::header::ToStrError> for $error_struct {
fn from(err: hyper::header::ToStrError) -> Self {
Self::Common(CommonError::InvalidHeader(err))
}
}
impl CommonErrorDerivative for $error_struct {}
};
}
pub use commonErrorDerivative;
impl CommonError {
pub fn http_status_code(&self) -> StatusCode {
match self {
CommonError::InternalError(
GarageError::Timeout
| GarageError::RemoteError(_)
| GarageError::Quorum(_, _, _, _),
GarageError::Timeout | GarageError::RemoteError(_) | GarageError::Quorum(..),
) => StatusCode::SERVICE_UNAVAILABLE,
CommonError::InternalError(_) | CommonError::Hyper(_) | CommonError::Http(_) => {
StatusCode::INTERNAL_SERVER_ERROR
@@ -80,9 +109,7 @@ impl CommonError {
match self {
CommonError::Forbidden(_) => "AccessDenied",
CommonError::InternalError(
GarageError::Timeout
| GarageError::RemoteError(_)
| GarageError::Quorum(_, _, _, _),
GarageError::Timeout | GarageError::RemoteError(_) | GarageError::Quorum(..),
) => "ServiceUnavailable",
CommonError::InternalError(_) | CommonError::Hyper(_) | CommonError::Http(_) => {
"InternalError"
@@ -101,18 +128,39 @@ impl CommonError {
}
}
impl From<HelperError> for CommonError {
fn from(err: HelperError) -> Self {
impl TryFrom<HelperError> for CommonError {
type Error = HelperError;
fn try_from(err: HelperError) -> Result<Self, HelperError> {
match err {
HelperError::Internal(i) => Self::InternalError(i),
HelperError::BadRequest(b) => Self::BadRequest(b),
HelperError::InvalidBucketName(n) => Self::InvalidBucketName(n),
HelperError::NoSuchBucket(n) => Self::NoSuchBucket(n),
e => Self::bad_request(format!("{}", e)),
HelperError::Internal(i) => Ok(Self::InternalError(i)),
HelperError::BadRequest(b) => Ok(Self::BadRequest(b)),
HelperError::InvalidBucketName(n) => Ok(Self::InvalidBucketName(n)),
HelperError::NoSuchBucket(n) => Ok(Self::NoSuchBucket(n)),
e => Err(e),
}
}
}
/// This function converts HelperErrors into CommonErrors,
/// for variants that exist in CommonError.
/// This is used for helper functions that might return InvalidBucketName
/// or NoSuchBucket for instance, and we want to pass that error
/// up to our caller.
pub fn pass_helper_error(err: HelperError) -> CommonError {
match CommonError::try_from(err) {
Ok(e) => e,
Err(e) => panic!("Helper error `{}` should hot have happenned here", e),
}
}
pub fn helper_error_as_internal(err: HelperError) -> CommonError {
match err {
HelperError::Internal(e) => CommonError::InternalError(e),
e => CommonError::InternalError(GarageError::Message(e.to_string())),
}
}
pub trait CommonErrorDerivative: From<CommonError> {
fn internal_error<M: ToString>(msg: M) -> Self {
Self::from(CommonError::InternalError(GarageError::Message(
+160
View File
@@ -0,0 +1,160 @@
use std::sync::Arc;
use http::header::{
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN,
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD,
};
use hyper::{body::Body, body::Incoming as IncomingBody, Request, Response, StatusCode};
use garage_model::bucket_table::{BucketParams, CorsRule as GarageCorsRule};
use garage_model::garage::Garage;
use crate::common_error::{CommonError, OkOrBadRequest, OkOrInternalError};
use crate::helpers::*;
pub fn find_matching_cors_rule<'a, B>(
bucket_params: &'a BucketParams,
req: &Request<B>,
) -> Result<Option<&'a GarageCorsRule>, CommonError> {
if let Some(cors_config) = bucket_params.cors_config.get() {
if let Some(origin) = req.headers().get("Origin") {
let origin = origin.to_str()?;
let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) {
Some(h) => h.to_str()?.split(',').map(|h| h.trim()).collect::<Vec<_>>(),
None => vec![],
};
return Ok(cors_config.iter().find(|rule| {
cors_rule_matches(rule, origin, req.method().as_ref(), request_headers.iter())
}));
}
}
Ok(None)
}
pub fn cors_rule_matches<'a, HI, S>(
rule: &GarageCorsRule,
origin: &'a str,
method: &'a str,
mut request_headers: HI,
) -> bool
where
HI: Iterator<Item = S>,
S: AsRef<str>,
{
rule.allow_origins.iter().any(|x| x == "*" || x == origin)
&& rule.allow_methods.iter().any(|x| x == "*" || x == method)
&& request_headers.all(|h| {
rule.allow_headers
.iter()
.any(|x| x == "*" || x == h.as_ref())
})
}
pub fn add_cors_headers(
resp: &mut Response<impl Body>,
rule: &GarageCorsRule,
) -> Result<(), http::header::InvalidHeaderValue> {
let h = resp.headers_mut();
h.insert(
ACCESS_CONTROL_ALLOW_ORIGIN,
rule.allow_origins.join(", ").parse()?,
);
h.insert(
ACCESS_CONTROL_ALLOW_METHODS,
rule.allow_methods.join(", ").parse()?,
);
h.insert(
ACCESS_CONTROL_ALLOW_HEADERS,
rule.allow_headers.join(", ").parse()?,
);
h.insert(
ACCESS_CONTROL_EXPOSE_HEADERS,
rule.expose_headers.join(", ").parse()?,
);
Ok(())
}
pub fn handle_options_api(
garage: Arc<Garage>,
req: &Request<IncomingBody>,
bucket_name: Option<String>,
) -> Result<Response<EmptyBody>, CommonError> {
// FIXME: CORS rules of buckets with local aliases are
// not taken into account.
// If the bucket name is a global bucket name,
// we try to apply the CORS rules of that bucket.
// If a user has a local bucket name that has
// the same name, its CORS rules won't be applied
// and will be shadowed by the rules of the globally
// existing bucket (but this is inevitable because
// OPTIONS calls are not auhtenticated).
if let Some(bn) = bucket_name {
let helper = garage.bucket_helper();
let bucket_opt = helper.resolve_global_bucket_fast(&bn)?;
if let Some(bucket) = bucket_opt {
let bucket_params = bucket.state.into_option().unwrap();
handle_options_for_bucket(req, &bucket_params)
} else {
// If there is a bucket name in the request, but that name
// does not correspond to a global alias for a bucket,
// then it's either a non-existing bucket or a local bucket.
// We have no way of knowing, because the request is not
// authenticated and thus we can't resolve local aliases.
// We take the permissive approach of allowing everything,
// because we don't want to prevent web apps that use
// local bucket names from making API calls.
Ok(Response::builder()
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.header(ACCESS_CONTROL_ALLOW_METHODS, "*")
.status(StatusCode::OK)
.body(EmptyBody::new())?)
}
} else {
// If there is no bucket name in the request,
// we are doing a ListBuckets call, which we want to allow
// for all origins.
Ok(Response::builder()
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.header(ACCESS_CONTROL_ALLOW_METHODS, "GET")
.status(StatusCode::OK)
.body(EmptyBody::new())?)
}
}
pub fn handle_options_for_bucket<B>(
req: &Request<B>,
bucket_params: &BucketParams,
) -> Result<Response<EmptyBody>, CommonError> {
let origin = req
.headers()
.get("Origin")
.ok_or_bad_request("Missing Origin header")?
.to_str()?;
let request_method = req
.headers()
.get(ACCESS_CONTROL_REQUEST_METHOD)
.ok_or_bad_request("Missing Access-Control-Request-Method header")?
.to_str()?;
let request_headers = match req.headers().get(ACCESS_CONTROL_REQUEST_HEADERS) {
Some(h) => h.to_str()?.split(',').map(|h| h.trim()).collect::<Vec<_>>(),
None => vec![],
};
if let Some(cors_config) = bucket_params.cors_config.get() {
let matching_rule = cors_config
.iter()
.find(|rule| cors_rule_matches(rule, origin, request_method, request_headers.iter()));
if let Some(rule) = matching_rule {
let mut resp = Response::builder()
.status(StatusCode::OK)
.body(EmptyBody::new())?;
add_cors_headers(&mut resp, rule).ok_or_internal_error("Invalid CORS configuration")?;
return Ok(resp);
}
}
Err(CommonError::Forbidden(
"This CORS request is not allowed.".into(),
))
}
@@ -1,9 +1,9 @@
use std::borrow::Cow;
use std::convert::Infallible;
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
use std::sync::Arc;
use async_trait::async_trait;
use std::time::Duration;
use futures::future::Future;
use futures::stream::{futures_unordered::FuturesUnordered, StreamExt};
@@ -19,6 +19,7 @@ use hyper_util::rt::TokioIo;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpListener, TcpStream, UnixListener, UnixStream};
use tokio::sync::watch;
use tokio::time::{sleep_until, Instant};
use opentelemetry::{
global,
@@ -34,8 +35,8 @@ use garage_util::socket_address::UnixOrTCPSocketAddress;
use crate::helpers::{BoxBody, ErrorBody};
pub(crate) trait ApiEndpoint: Send + Sync + 'static {
fn name(&self) -> &'static str;
pub trait ApiEndpoint: Send + Sync + 'static {
fn name(&self) -> Cow<'static, str>;
fn add_span_attributes(&self, span: SpanRef<'_>);
}
@@ -45,8 +46,7 @@ pub trait ApiError: std::error::Error + Send + Sync + 'static {
fn http_body(&self, garage_region: &str, path: &str) -> ErrorBody;
}
#[async_trait]
pub(crate) trait ApiHandler: Send + Sync + 'static {
pub trait ApiHandler: Send + Sync + 'static {
const API_NAME: &'static str;
const API_NAME_DISPLAY: &'static str;
@@ -54,14 +54,14 @@ pub(crate) trait ApiHandler: Send + Sync + 'static {
type Error: ApiError;
fn parse_endpoint(&self, r: &Request<IncomingBody>) -> Result<Self::Endpoint, Self::Error>;
async fn handle(
fn handle(
&self,
req: Request<IncomingBody>,
endpoint: Self::Endpoint,
) -> Result<Response<BoxBody<Self::Error>>, Self::Error>;
) -> impl Future<Output = Result<Response<BoxBody<Self::Error>>, Self::Error>> + Send;
}
pub(crate) struct ApiServer<A: ApiHandler> {
pub struct ApiServer<A: ApiHandler> {
region: String,
api_handler: A,
@@ -246,13 +246,11 @@ impl<A: ApiHandler> ApiServer<A> {
// ==== helper functions ====
#[async_trait]
pub trait Accept: Send + Sync + 'static {
type Stream: AsyncRead + AsyncWrite + Send + Sync + 'static;
async fn accept(&self) -> std::io::Result<(Self::Stream, String)>;
fn accept(&self) -> impl Future<Output = std::io::Result<(Self::Stream, String)>> + Send;
}
#[async_trait]
impl Accept for TcpListener {
type Stream = TcpStream;
async fn accept(&self) -> std::io::Result<(Self::Stream, String)> {
@@ -264,7 +262,6 @@ impl Accept for TcpListener {
pub struct UnixListenerOn(pub UnixListener, pub String);
#[async_trait]
impl Accept for UnixListenerOn {
type Stream = UnixStream;
async fn accept(&self) -> std::io::Result<(Self::Stream, String)> {
@@ -291,7 +288,7 @@ where
let connection_collector = tokio::spawn({
let server_name = server_name.clone();
async move {
let mut connections = FuturesUnordered::new();
let mut connections = FuturesUnordered::<tokio::task::JoinHandle<()>>::new();
loop {
let collect_next = async {
if connections.is_empty() {
@@ -312,23 +309,34 @@ where
}
}
}
if !connections.is_empty() {
let deadline = Instant::now() + Duration::from_secs(10);
while !connections.is_empty() {
info!(
"{} server: {} connections still open",
"{} server: {} connections still open, deadline in {:.2}s",
server_name,
connections.len()
connections.len(),
(deadline - Instant::now()).as_secs_f32(),
);
while let Some(conn_res) = connections.next().await {
trace!(
"{} server: HTTP connection finished: {:?}",
server_name,
conn_res
);
info!(
"{} server: {} connections still open",
server_name,
connections.len()
);
tokio::select! {
conn_res = connections.next() => {
trace!(
"{} server: HTTP connection finished: {:?}",
server_name,
conn_res.unwrap(),
);
}
_ = sleep_until(deadline) => {
warn!("{} server: exit deadline reached with {} connections still open, killing them now",
server_name,
connections.len());
for conn in connections.iter() {
conn.abort();
}
for conn in connections {
assert!(conn.await.unwrap_err().is_cancelled());
}
break;
}
}
}
}
@@ -8,7 +8,6 @@ use hyper::{
body::{Body, Bytes},
Request, Response,
};
use idna::domain_to_unicode;
use serde::{Deserialize, Serialize};
use garage_model::bucket_table::BucketParams;
@@ -97,7 +96,7 @@ pub fn authority_to_host(authority: &str) -> Result<String, Error> {
authority
))),
};
authority.map(|h| domain_to_unicode(h).0)
authority.map(|h| h.to_ascii_lowercase())
}
/// Extract the bucket name and the key name from an HTTP path and possibly a bucket provided in
@@ -363,9 +362,9 @@ mod tests {
}
#[derive(Serialize)]
pub(crate) struct CustomApiErrorBody {
pub(crate) code: String,
pub(crate) message: String,
pub(crate) region: String,
pub(crate) path: String,
pub struct CustomApiErrorBody {
pub code: String,
pub message: String,
pub region: String,
pub path: String,
}
+12
View File
@@ -0,0 +1,12 @@
//! Crate for serving a S3 compatible API
#[macro_use]
extern crate tracing;
pub mod common_error;
pub mod cors;
pub mod encoding;
pub mod generic_server;
pub mod helpers;
pub mod router_macros;
pub mod signature;
@@ -1,5 +1,6 @@
/// This macro is used to generate very repetitive match {} blocks in this module
/// It is _not_ made to be used anywhere else
#[macro_export]
macro_rules! router_match {
(@match $enum:expr , [ $($endpoint:ident,)* ]) => {{
// usage: router_match {@match my_enum, [ VariantWithField1, VariantWithField2 ..] }
@@ -44,6 +45,68 @@ macro_rules! router_match {
}
}
}};
(@gen_path_parser_v2 ($method:expr, $reqpath:expr, $pathprefix:literal, $query:expr, $req:expr)
[
$(@special $spec_meth:ident $spec_path:pat => $spec_api:ident $spec_params:tt,)*
$($meth:ident $api:ident $params:tt,)*
]) => {{
{
#[allow(unused_parens)]
match ($method, $reqpath) {
$(
(&Method::$spec_meth, $spec_path) => AdminApiRequest::$spec_api (
router_match!(@@gen_parse_request $spec_api, $spec_params, $query, $req)
),
)*
$(
(&Method::$meth, concat!($pathprefix, stringify!($api)))
=> AdminApiRequest::$api (
router_match!(@@gen_parse_request $api, $params, $query, $req)
),
)*
(m, p) => {
return Err(Error::bad_request(format!(
"Unknown API endpoint: {} {}",
m, p
)))
}
}
}
}};
(@@gen_parse_request $api:ident, (), $query: expr, $req:expr) => {{
paste!(
[< $api Request >]
)
}};
(@@gen_parse_request $api:ident, (body), $query: expr, $req:expr) => {{
paste!({
parse_json_body::< [<$api Request>], _, Error>($req).await?
})
}};
(@@gen_parse_request $api:ident, (body_field, $($conv:ident $(($conv_arg:expr))? :: $param:ident),*), $query: expr, $req:expr)
=>
{{
paste!({
let body = parse_json_body::< [<$api RequestBody>], _, Error>($req).await?;
[< $api Request >] {
body,
$(
$param: router_match!(@@parse_param $query, $conv $(($conv_arg))?, $param),
)+
}
})
}};
(@@gen_parse_request $api:ident, ($($conv:ident $(($conv_arg:expr))? :: $param:ident),*), $query: expr, $req:expr)
=>
{{
paste!({
[< $api Request >] {
$(
$param: router_match!(@@parse_param $query, $conv $(($conv_arg))?, $param),
)+
}
})
}};
(@gen_parser ($keyword:expr, $key:ident, $query:expr, $header:expr),
key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*],
no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{
@@ -78,13 +141,19 @@ macro_rules! router_match {
}
}};
(@@parse_param $query:expr, default, $param:ident) => {{
Default::default()
}};
(@@parse_param $query:expr, query_opt, $param:ident) => {{
// extract optional query parameter
$query.$param.take().map(|param| param.into_owned())
}};
(@@parse_param $query:expr, query, $param:ident) => {{
// extract mendatory query parameter
$query.$param.take().ok_or_bad_request("Missing argument for endpoint")?.into_owned()
$query.$param.take()
.ok_or_bad_request(
format!("Missing argument `{}` for endpoint", stringify!($param))
)?.into_owned()
}};
(@@parse_param $query:expr, opt_parse, $param:ident) => {{
// extract and parse optional query parameter
@@ -98,10 +167,22 @@ macro_rules! router_match {
(@@parse_param $query:expr, parse, $param:ident) => {{
// extract and parse mandatory query parameter
// both missing and un-parseable parameters are reported as errors
$query.$param.take().ok_or_bad_request("Missing argument for endpoint")?
$query.$param.take()
.ok_or_bad_request(
format!("Missing argument `{}` for endpoint", stringify!($param))
)?
.parse()
.map_err(|_| Error::bad_request("Failed to parse query parameter"))?
}};
(@@parse_param $query:expr, parse_default($default:expr), $param:ident) => {{
// extract and parse optional query parameter
// using provided value as default if paramter is missing
$query.$param.take().map(|x| x
.parse()
.map_err(|_| Error::bad_request("Failed to parse query parameter")))
.transpose()?
.unwrap_or($default)
}};
(@func
$(#[$doc:meta])*
pub enum Endpoint {
@@ -133,6 +214,7 @@ macro_rules! router_match {
/// This macro is used to generate part of the code in this module. It must be called only one, and
/// is useless outside of this module.
#[macro_export]
macro_rules! generateQueryParameters {
(
keywords: [ $($kw_param:expr => $kw_name: ident),* ],
@@ -185,6 +267,7 @@ macro_rules! generateQueryParameters {
},
)*
$(
// FIXME: remove if !v.is_empty() ?
$f_param => if !v.is_empty() {
if res.$f_name.replace(v).is_some() {
return Err(Error::bad_request(format!(
@@ -204,7 +287,7 @@ macro_rules! generateQueryParameters {
}
/// Get an error message in case not all parameters where used when extracting them to
/// build an Enpoint variant
/// build an Endpoint variant
fn nonempty_message(&self) -> Option<&str> {
if self.keyword.is_some() {
Some("Keyword not used")
@@ -220,5 +303,5 @@ macro_rules! generateQueryParameters {
}
}
pub(crate) use generateQueryParameters;
pub(crate) use router_match;
pub use generateQueryParameters;
pub use router_match;
+135
View File
@@ -0,0 +1,135 @@
use std::sync::Mutex;
use futures::prelude::*;
use futures::stream::BoxStream;
use http_body_util::{BodyExt, StreamBody};
use hyper::body::{Bytes, Frame};
use serde::Deserialize;
use tokio::sync::mpsc;
use tokio::task;
use super::*;
use crate::signature::checksum::*;
pub struct ReqBody {
// why need mutex to be sync??
pub(crate) stream: Mutex<BoxStream<'static, Result<Frame<Bytes>, Error>>>,
pub(crate) checksummer: Checksummer,
pub(crate) expected_checksums: ExpectedChecksums,
pub(crate) trailer_algorithm: Option<ChecksumAlgorithm>,
}
pub type StreamingChecksumReceiver = task::JoinHandle<Result<Checksums, Error>>;
impl ReqBody {
pub fn add_expected_checksums(&mut self, more: ExpectedChecksums) {
if more.md5.is_some() {
self.expected_checksums.md5 = more.md5;
}
if more.sha256.is_some() {
self.expected_checksums.sha256 = more.sha256;
}
if more.extra.is_some() {
self.expected_checksums.extra = more.extra;
}
self.checksummer.add_expected(&self.expected_checksums);
}
pub fn add_md5(&mut self) {
self.checksummer.add_md5();
}
// ============ non-streaming =============
pub async fn json<T: for<'a> Deserialize<'a>>(self) -> Result<T, Error> {
let body = self.collect().await?;
let resp: T = serde_json::from_slice(&body).ok_or_bad_request("Invalid JSON")?;
Ok(resp)
}
pub async fn collect(self) -> Result<Bytes, Error> {
self.collect_with_checksums().await.map(|(b, _)| b)
}
pub async fn collect_with_checksums(mut self) -> Result<(Bytes, Checksums), Error> {
let stream: BoxStream<_> = self.stream.into_inner().unwrap();
let bytes = BodyExt::collect(StreamBody::new(stream)).await?.to_bytes();
self.checksummer.update(&bytes);
let checksums = self.checksummer.finalize();
checksums.verify(&self.expected_checksums)?;
Ok((bytes, checksums))
}
// ============ streaming =============
pub fn streaming_with_checksums(
self,
) -> (
BoxStream<'static, Result<Bytes, Error>>,
StreamingChecksumReceiver,
) {
let Self {
stream,
mut checksummer,
mut expected_checksums,
trailer_algorithm,
} = self;
let (frame_tx, mut frame_rx) = mpsc::channel::<Frame<Bytes>>(5);
let join_checksums = tokio::spawn(async move {
while let Some(frame) = frame_rx.recv().await {
match frame.into_data() {
Ok(data) => {
checksummer = tokio::task::spawn_blocking(move || {
checksummer.update(&data);
checksummer
})
.await
.unwrap()
}
Err(frame) => {
let trailers = frame.into_trailers().unwrap();
let algo = trailer_algorithm.unwrap();
expected_checksums.extra = Some(extract_checksum_value(&trailers, algo)?);
break;
}
}
}
if trailer_algorithm.is_some() && expected_checksums.extra.is_none() {
return Err(Error::bad_request("trailing checksum was not sent"));
}
let checksums = checksummer.finalize();
checksums.verify(&expected_checksums)?;
Ok(checksums)
});
let stream: BoxStream<_> = stream.into_inner().unwrap();
let stream = stream.filter_map(move |x| {
let frame_tx = frame_tx.clone();
async move {
match x {
Err(e) => Some(Err(e)),
Ok(frame) => {
if frame.is_data() {
let data = frame.data_ref().unwrap().clone();
let _ = frame_tx.send(frame).await;
Some(Ok(data))
} else {
let _ = frame_tx.send(frame).await;
None
}
}
}
}
});
(stream.boxed(), join_checksums)
}
}
+378
View File
@@ -0,0 +1,378 @@
use std::convert::TryInto;
use base64::prelude::*;
use crc_fast::{CrcAlgorithm, Digest as CrcDigest};
use md5::{Digest, Md5};
use sha1::Sha1;
use sha2::Sha256;
use http::{HeaderMap, HeaderName, HeaderValue};
use garage_util::data::*;
use super::*;
pub use garage_model::s3::object_table::{ChecksumAlgorithm, ChecksumValue};
pub const CONTENT_MD5: HeaderName = HeaderName::from_static("content-md5");
pub const X_AMZ_CHECKSUM_ALGORITHM: HeaderName =
HeaderName::from_static("x-amz-checksum-algorithm");
pub const X_AMZ_CHECKSUM_MODE: HeaderName = HeaderName::from_static("x-amz-checksum-mode");
pub const X_AMZ_CHECKSUM_TYPE: HeaderName = HeaderName::from_static("x-amz-checksum-type");
pub const X_AMZ_CHECKSUM_CRC32: HeaderName = HeaderName::from_static("x-amz-checksum-crc32");
pub const X_AMZ_CHECKSUM_CRC32C: HeaderName = HeaderName::from_static("x-amz-checksum-crc32c");
pub const X_AMZ_CHECKSUM_CRC64NVME: HeaderName =
HeaderName::from_static("x-amz-checksum-crc64nvme");
pub const X_AMZ_CHECKSUM_SHA1: HeaderName = HeaderName::from_static("x-amz-checksum-sha1");
pub const X_AMZ_CHECKSUM_SHA256: HeaderName = HeaderName::from_static("x-amz-checksum-sha256");
// Values for x-amz-checksum-type
pub const COMPOSITE: &str = "COMPOSITE";
pub const FULL_OBJECT: &str = "FULL_OBJECT";
pub type Crc32Checksum = [u8; 4];
pub type Crc32cChecksum = [u8; 4];
pub type Crc64NvmeChecksum = [u8; 8];
pub type Md5Checksum = [u8; 16];
pub type Sha1Checksum = [u8; 20];
pub type Sha256Checksum = [u8; 32];
// -- MAP OF CRC ALGORITHMS :
// CRC32 -> CrcAlgorithm::Crc32IsoHdlc
// CRC32C -> CrcAlgorithm::Crc32Iscsi
// CRC64NVME -> CrcAlgorithm::Crc64Nvme
pub fn new_crc32() -> CrcDigest {
CrcDigest::new(CrcAlgorithm::Crc32IsoHdlc)
}
pub fn new_crc32c() -> CrcDigest {
CrcDigest::new(CrcAlgorithm::Crc32Iscsi)
}
pub fn new_crc64nvme() -> CrcDigest {
CrcDigest::new(CrcAlgorithm::Crc64Nvme)
}
#[derive(Debug, Default, Clone)]
pub struct ExpectedChecksums {
// base64-encoded md5 (content-md5 header)
pub md5: Option<String>,
// content_sha256 (as a Hash / FixedBytes32)
pub sha256: Option<Hash>,
// extra x-amz-checksum-* header
pub extra: Option<ChecksumValue>,
}
pub struct Checksummer {
pub crc32: Option<CrcDigest>,
pub crc32c: Option<CrcDigest>,
pub crc64nvme: Option<CrcDigest>,
pub md5: Option<Md5>,
pub sha1: Option<Sha1>,
pub sha256: Option<Sha256>,
}
#[derive(Default)]
pub struct Checksums {
pub crc32: Option<Crc32Checksum>,
pub crc32c: Option<Crc32cChecksum>,
pub crc64nvme: Option<Crc64NvmeChecksum>,
pub md5: Option<Md5Checksum>,
pub sha1: Option<Sha1Checksum>,
pub sha256: Option<Sha256Checksum>,
}
impl Checksummer {
pub fn new() -> Self {
Self {
crc32: None,
crc32c: None,
crc64nvme: None,
md5: None,
sha1: None,
sha256: None,
}
}
pub fn init(expected: &ExpectedChecksums, add_md5: bool) -> Self {
let mut ret = Self::new();
ret.add_expected(expected);
if add_md5 {
ret.add_md5();
}
ret
}
pub fn add_md5(&mut self) {
self.md5 = Some(Md5::new());
}
pub fn add_expected(&mut self, expected: &ExpectedChecksums) {
if expected.md5.is_some() {
self.md5 = Some(Md5::new());
}
if expected.sha256.is_some() || matches!(&expected.extra, Some(ChecksumValue::Sha256(_))) {
self.sha256 = Some(Sha256::new());
}
if matches!(&expected.extra, Some(ChecksumValue::Crc32(_))) {
self.crc32 = Some(new_crc32());
}
if matches!(&expected.extra, Some(ChecksumValue::Crc32c(_))) {
self.crc32c = Some(new_crc32c());
}
if matches!(&expected.extra, Some(ChecksumValue::Crc64Nvme(_))) {
self.crc64nvme = Some(new_crc64nvme());
}
if matches!(&expected.extra, Some(ChecksumValue::Sha1(_))) {
self.sha1 = Some(Sha1::new());
}
}
pub fn add(mut self, algo: Option<ChecksumAlgorithm>) -> Self {
match algo {
Some(ChecksumAlgorithm::Crc32) => {
self.crc32 = Some(new_crc32());
}
Some(ChecksumAlgorithm::Crc32c) => {
self.crc32c = Some(new_crc32c());
}
Some(ChecksumAlgorithm::Crc64Nvme) => {
self.crc64nvme = Some(new_crc64nvme());
}
Some(ChecksumAlgorithm::Sha1) => {
self.sha1 = Some(Sha1::new());
}
Some(ChecksumAlgorithm::Sha256) => {
self.sha256 = Some(Sha256::new());
}
None => (),
}
self
}
pub fn update(&mut self, bytes: &[u8]) {
if let Some(crc32) = &mut self.crc32 {
crc32.update(bytes);
}
if let Some(crc32c) = &mut self.crc32c {
crc32c.update(bytes);
}
if let Some(crc64nvme) = &mut self.crc64nvme {
crc64nvme.update(bytes);
}
if let Some(md5) = &mut self.md5 {
md5.update(bytes);
}
if let Some(sha1) = &mut self.sha1 {
sha1.update(bytes);
}
if let Some(sha256) = &mut self.sha256 {
sha256.update(bytes);
}
}
pub fn finalize(self) -> Checksums {
Checksums {
crc32: self.crc32.map(|x| u32::to_be_bytes(x.finalize() as u32)),
crc32c: self.crc32c.map(|x| u32::to_be_bytes(x.finalize() as u32)),
crc64nvme: self.crc64nvme.map(|x| u64::to_be_bytes(x.finalize())),
md5: self.md5.map(|x| x.finalize()[..].try_into().unwrap()),
sha1: self.sha1.map(|x| x.finalize()[..].try_into().unwrap()),
sha256: self.sha256.map(|x| x.finalize()[..].try_into().unwrap()),
}
}
}
impl Checksums {
pub fn verify(&self, expected: &ExpectedChecksums) -> Result<(), Error> {
if let Some(expected_md5) = &expected.md5 {
match self.md5 {
Some(md5) if BASE64_STANDARD.encode(&md5) == expected_md5.trim_matches('"') => (),
_ => {
return Err(Error::InvalidDigest(
"MD5 checksum verification failed (from content-md5)".into(),
))
}
}
}
if let Some(expected_sha256) = &expected.sha256 {
match self.sha256 {
Some(sha256) if &sha256[..] == expected_sha256.as_slice() => (),
_ => {
return Err(Error::InvalidDigest(
"SHA256 checksum verification failed (from x-amz-content-sha256)".into(),
))
}
}
}
if let Some(extra) = expected.extra {
let algo = extra.algorithm();
let calculated = self.extract(Some(algo));
if calculated != Some(extra) {
return Err(Error::InvalidDigest(format!(
"Failed to validate checksum for algorithm {:?}: calculated {:?}, expected {:?}",
algo, calculated, extra
)));
}
}
Ok(())
}
pub fn extract(&self, algo: Option<ChecksumAlgorithm>) -> Option<ChecksumValue> {
match algo {
None => None,
Some(ChecksumAlgorithm::Crc32) => Some(ChecksumValue::Crc32(self.crc32.unwrap())),
Some(ChecksumAlgorithm::Crc32c) => Some(ChecksumValue::Crc32c(self.crc32c.unwrap())),
Some(ChecksumAlgorithm::Crc64Nvme) => {
Some(ChecksumValue::Crc64Nvme(self.crc64nvme.unwrap()))
}
Some(ChecksumAlgorithm::Sha1) => Some(ChecksumValue::Sha1(self.sha1.unwrap())),
Some(ChecksumAlgorithm::Sha256) => Some(ChecksumValue::Sha256(self.sha256.unwrap())),
}
}
}
// ----
pub fn parse_checksum_algorithm(algo: &str) -> Result<ChecksumAlgorithm, Error> {
match algo {
"CRC32" => Ok(ChecksumAlgorithm::Crc32),
"CRC32C" => Ok(ChecksumAlgorithm::Crc32c),
"CRC64NVME" => Ok(ChecksumAlgorithm::Crc64Nvme),
"SHA1" => Ok(ChecksumAlgorithm::Sha1),
"SHA256" => Ok(ChecksumAlgorithm::Sha256),
_ => Err(Error::bad_request("invalid checksum algorithm")),
}
}
/// Extract the value of the x-amz-checksum-algorithm header
pub fn request_checksum_algorithm(
headers: &HeaderMap<HeaderValue>,
) -> Result<Option<ChecksumAlgorithm>, Error> {
match headers.get(X_AMZ_CHECKSUM_ALGORITHM) {
None => Ok(None),
Some(x) => parse_checksum_algorithm(x.to_str()?).map(Some),
}
}
pub fn request_trailer_checksum_algorithm(
headers: &HeaderMap<HeaderValue>,
) -> Result<Option<ChecksumAlgorithm>, Error> {
match headers.get(X_AMZ_TRAILER).map(|x| x.to_str()).transpose()? {
None => Ok(None),
Some(x) if x == X_AMZ_CHECKSUM_CRC32 => Ok(Some(ChecksumAlgorithm::Crc32)),
Some(x) if x == X_AMZ_CHECKSUM_CRC32C => Ok(Some(ChecksumAlgorithm::Crc32c)),
Some(x) if x == X_AMZ_CHECKSUM_CRC64NVME => Ok(Some(ChecksumAlgorithm::Crc64Nvme)),
Some(x) if x == X_AMZ_CHECKSUM_SHA1 => Ok(Some(ChecksumAlgorithm::Sha1)),
Some(x) if x == X_AMZ_CHECKSUM_SHA256 => Ok(Some(ChecksumAlgorithm::Sha256)),
_ => Err(Error::bad_request("invalid checksum algorithm")),
}
}
/// Extract the value of any of the x-amz-checksum-* headers
pub fn request_checksum_value(
headers: &HeaderMap<HeaderValue>,
) -> Result<Option<ChecksumValue>, Error> {
let mut ret = vec![];
if headers.contains_key(X_AMZ_CHECKSUM_CRC32) {
ret.push(extract_checksum_value(headers, ChecksumAlgorithm::Crc32)?);
}
if headers.contains_key(X_AMZ_CHECKSUM_CRC32C) {
ret.push(extract_checksum_value(headers, ChecksumAlgorithm::Crc32c)?);
}
if headers.contains_key(X_AMZ_CHECKSUM_CRC64NVME) {
ret.push(extract_checksum_value(
headers,
ChecksumAlgorithm::Crc64Nvme,
)?);
}
if headers.contains_key(X_AMZ_CHECKSUM_SHA1) {
ret.push(extract_checksum_value(headers, ChecksumAlgorithm::Sha1)?);
}
if headers.contains_key(X_AMZ_CHECKSUM_SHA256) {
ret.push(extract_checksum_value(headers, ChecksumAlgorithm::Sha256)?);
}
if ret.len() > 1 {
return Err(Error::bad_request(
"multiple x-amz-checksum-* headers given",
));
}
Ok(ret.pop())
}
/// Checks for the presence of x-amz-checksum-algorithm
/// if so extract the corresponding x-amz-checksum-* value
pub fn extract_checksum_value(
headers: &HeaderMap<HeaderValue>,
algo: ChecksumAlgorithm,
) -> Result<ChecksumValue, Error> {
match algo {
ChecksumAlgorithm::Crc32 => {
let crc32 = headers
.get(X_AMZ_CHECKSUM_CRC32)
.and_then(|x| BASE64_STANDARD.decode(&x).ok())
.and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-crc32 header")?;
Ok(ChecksumValue::Crc32(crc32))
}
ChecksumAlgorithm::Crc32c => {
let crc32c = headers
.get(X_AMZ_CHECKSUM_CRC32C)
.and_then(|x| BASE64_STANDARD.decode(&x).ok())
.and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-crc32c header")?;
Ok(ChecksumValue::Crc32c(crc32c))
}
ChecksumAlgorithm::Crc64Nvme => {
let crc64nvme = headers
.get(X_AMZ_CHECKSUM_CRC64NVME)
.and_then(|x| BASE64_STANDARD.decode(&x).ok())
.and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-crc64nvme header")?;
Ok(ChecksumValue::Crc64Nvme(crc64nvme))
}
ChecksumAlgorithm::Sha1 => {
let sha1 = headers
.get(X_AMZ_CHECKSUM_SHA1)
.and_then(|x| BASE64_STANDARD.decode(&x).ok())
.and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-sha1 header")?;
Ok(ChecksumValue::Sha1(sha1))
}
ChecksumAlgorithm::Sha256 => {
let sha256 = headers
.get(X_AMZ_CHECKSUM_SHA256)
.and_then(|x| BASE64_STANDARD.decode(&x).ok())
.and_then(|x| x.try_into().ok())
.ok_or_bad_request("invalid x-amz-checksum-sha256 header")?;
Ok(ChecksumValue::Sha256(sha256))
}
}
}
pub fn add_checksum_response_headers(
checksum: &Option<ChecksumValue>,
mut resp: http::response::Builder,
) -> http::response::Builder {
match checksum {
Some(ChecksumValue::Crc32(crc32)) => {
resp = resp.header(X_AMZ_CHECKSUM_CRC32, BASE64_STANDARD.encode(&crc32));
}
Some(ChecksumValue::Crc32c(crc32c)) => {
resp = resp.header(X_AMZ_CHECKSUM_CRC32C, BASE64_STANDARD.encode(&crc32c));
}
Some(ChecksumValue::Crc64Nvme(crc64nvme)) => {
resp = resp.header(X_AMZ_CHECKSUM_CRC64NVME, BASE64_STANDARD.encode(&crc64nvme));
}
Some(ChecksumValue::Sha1(sha1)) => {
resp = resp.header(X_AMZ_CHECKSUM_SHA1, BASE64_STANDARD.encode(&sha1));
}
Some(ChecksumValue::Sha256(sha256)) => {
resp = resp.header(X_AMZ_CHECKSUM_SHA256, BASE64_STANDARD.encode(&sha256));
}
None => (),
}
resp
}
@@ -18,6 +18,10 @@ pub enum Error {
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
#[error(display = "Invalid UTF-8: {}", _0)]
InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
/// The provided digest (checksum) value was invalid
#[error(display = "Invalid digest: {}", _0)]
InvalidDigest(String),
}
impl<T> From<T> for Error
+118
View File
@@ -0,0 +1,118 @@
use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use hyper::header::HeaderName;
use hyper::{body::Incoming as IncomingBody, Request};
use garage_model::garage::Garage;
use garage_model::key_table::Key;
use garage_util::data::{sha256sum, Hash};
use error::*;
pub mod body;
pub mod checksum;
pub mod error;
pub mod payload;
pub mod streaming;
pub const SHORT_DATE: &str = "%Y%m%d";
pub const LONG_DATETIME: &str = "%Y%m%dT%H%M%SZ";
// ---- Constants used in AWSv4 signatures ----
pub const X_AMZ_ALGORITHM: HeaderName = HeaderName::from_static("x-amz-algorithm");
pub const X_AMZ_CREDENTIAL: HeaderName = HeaderName::from_static("x-amz-credential");
pub const X_AMZ_DATE: HeaderName = HeaderName::from_static("x-amz-date");
pub const X_AMZ_EXPIRES: HeaderName = HeaderName::from_static("x-amz-expires");
pub const X_AMZ_SIGNEDHEADERS: HeaderName = HeaderName::from_static("x-amz-signedheaders");
pub const X_AMZ_SIGNATURE: HeaderName = HeaderName::from_static("x-amz-signature");
pub const X_AMZ_CONTENT_SHA256: HeaderName = HeaderName::from_static("x-amz-content-sha256");
pub const X_AMZ_TRAILER: HeaderName = HeaderName::from_static("x-amz-trailer");
/// Result of `sha256("")`
pub(crate) const EMPTY_STRING_HEX_DIGEST: &str =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
// Signature calculation algorithm
pub const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256";
type HmacSha256 = Hmac<Sha256>;
// Possible values for x-amz-content-sha256, in addition to the actual sha256
pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
pub const STREAMING_UNSIGNED_PAYLOAD_TRAILER: &str = "STREAMING-UNSIGNED-PAYLOAD-TRAILER";
pub const STREAMING_AWS4_HMAC_SHA256_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
// Used in the computation of StringToSign
pub const AWS4_HMAC_SHA256_PAYLOAD: &str = "AWS4-HMAC-SHA256-PAYLOAD";
// ---- enums to describe stuff going on in signature calculation ----
#[derive(Debug)]
pub enum ContentSha256Header {
UnsignedPayload,
Sha256Checksum(Hash),
StreamingPayload { trailer: bool, signed: bool },
}
// ---- top-level functions ----
pub struct VerifiedRequest {
pub request: Request<streaming::ReqBody>,
pub access_key: Key,
pub content_sha256_header: ContentSha256Header,
}
pub fn verify_request(
garage: &Garage,
mut req: Request<IncomingBody>,
service: &'static str,
) -> Result<VerifiedRequest, Error> {
let checked_signature = payload::check_payload_signature(&garage, &mut req, service)?;
let request = streaming::parse_streaming_body(
req,
&checked_signature,
&garage.config.s3_api.s3_region,
service,
)?;
let access_key = checked_signature
.key
.ok_or_else(|| Error::forbidden("Garage does not support anonymous access yet"))?;
Ok(VerifiedRequest {
request,
access_key,
content_sha256_header: checked_signature.content_sha256_header,
})
}
pub fn signing_hmac(
datetime: &DateTime<Utc>,
secret_key: &str,
region: &str,
service: &str,
) -> Result<HmacSha256, crypto_common::InvalidLength> {
let secret = String::from("AWS4") + secret_key;
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_from_slice(&date_hmac.finalize().into_bytes())?;
region_hmac.update(region.as_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_from_slice(&service_hmac.finalize().into_bytes())?;
signing_hmac.update(b"aws4_request");
let hmac = HmacSha256::new_from_slice(&signing_hmac.finalize().into_bytes())?;
Ok(hmac)
}
pub fn compute_scope(datetime: &DateTime<Utc>, region: &str, service: &str) -> String {
format!(
"{}/{}/{}/aws4_request",
datetime.format(SHORT_DATE),
region,
service
)
}
@@ -9,27 +9,14 @@ use sha2::{Digest, Sha256};
use garage_table::*;
use garage_util::data::Hash;
use garage_util::time::now_msec;
use garage_model::garage::Garage;
use garage_model::key_table::*;
use super::LONG_DATETIME;
use super::{compute_scope, signing_hmac};
use super::*;
use crate::encoding::uri_encode;
use crate::signature::error::*;
pub const X_AMZ_ALGORITHM: HeaderName = HeaderName::from_static("x-amz-algorithm");
pub const X_AMZ_CREDENTIAL: HeaderName = HeaderName::from_static("x-amz-credential");
pub const X_AMZ_DATE: HeaderName = HeaderName::from_static("x-amz-date");
pub const X_AMZ_EXPIRES: HeaderName = HeaderName::from_static("x-amz-expires");
pub const X_AMZ_SIGNEDHEADERS: HeaderName = HeaderName::from_static("x-amz-signedheaders");
pub const X_AMZ_SIGNATURE: HeaderName = HeaderName::from_static("x-amz-signature");
pub const X_AMZ_CONTENT_SH256: HeaderName = HeaderName::from_static("x-amz-content-sha256");
pub const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256";
pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
pub const STREAMING_AWS4_HMAC_SHA256_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
pub type QueryMap = HeaderMap<QueryValue>;
pub struct QueryValue {
@@ -39,44 +26,80 @@ pub struct QueryValue {
value: String,
}
pub async fn check_payload_signature(
#[derive(Debug)]
pub struct CheckedSignature {
pub key: Option<Key>,
pub content_sha256_header: ContentSha256Header,
pub signature_header: Option<String>,
}
pub fn check_payload_signature(
garage: &Garage,
request: &mut Request<IncomingBody>,
service: &'static str,
) -> Result<(Option<Key>, Option<Hash>), Error> {
) -> Result<CheckedSignature, Error> {
let query = parse_query_map(request.uri())?;
if query.contains_key(&X_AMZ_ALGORITHM) {
// We check for presigned-URL-style authentification first, because
// the browser or someting else could inject an Authorization header
// We check for presigned-URL-style authentication first, because
// the browser or something else could inject an Authorization header
// that is totally unrelated to AWS signatures.
check_presigned_signature(garage, service, request, query).await
check_presigned_signature(garage, service, request, query)
} else if request.headers().contains_key(AUTHORIZATION) {
check_standard_signature(garage, service, request, query).await
check_standard_signature(garage, service, request, query)
} else {
// Unsigned (anonymous) request
let content_sha256 = request
.headers()
.get("x-amz-content-sha256")
.filter(|c| c.as_bytes() != UNSIGNED_PAYLOAD.as_bytes());
if let Some(content_sha256) = content_sha256 {
let sha256 = hex::decode(content_sha256)
.ok()
.and_then(|bytes| Hash::try_from(&bytes))
.ok_or_bad_request("Invalid content sha256 hash")?;
Ok((None, Some(sha256)))
} else {
Ok((None, None))
}
.get(X_AMZ_CONTENT_SHA256)
.map(|x| x.to_str())
.transpose()?;
Ok(CheckedSignature {
key: None,
content_sha256_header: parse_x_amz_content_sha256(content_sha256)?,
signature_header: None,
})
}
}
async fn check_standard_signature(
fn parse_x_amz_content_sha256(header: Option<&str>) -> Result<ContentSha256Header, Error> {
let header = match header {
Some(x) => x,
None => return Ok(ContentSha256Header::UnsignedPayload),
};
if header == UNSIGNED_PAYLOAD {
Ok(ContentSha256Header::UnsignedPayload)
} else if let Some(rest) = header.strip_prefix("STREAMING-") {
let (trailer, algo) = if let Some(rest2) = rest.strip_suffix("-TRAILER") {
(true, rest2)
} else {
(false, rest)
};
let signed = match algo {
AWS4_HMAC_SHA256_PAYLOAD => true,
UNSIGNED_PAYLOAD => false,
_ => {
return Err(Error::bad_request(
"invalid or unsupported x-amz-content-sha256",
))
}
};
Ok(ContentSha256Header::StreamingPayload { trailer, signed })
} else {
let sha256 = hex::decode(header)
.ok()
.and_then(|bytes| Hash::try_from(&bytes))
.ok_or_bad_request("Invalid content sha256 hash")?;
Ok(ContentSha256Header::Sha256Checksum(sha256))
}
}
fn check_standard_signature(
garage: &Garage,
service: &'static str,
request: &Request<IncomingBody>,
query: QueryMap,
) -> Result<(Option<Key>, Option<Hash>), Error> {
) -> Result<CheckedSignature, Error> {
let authorization = Authorization::parse_header(request.headers())?;
// Verify that all necessary request headers are included in signed_headers
@@ -106,33 +129,28 @@ async fn check_standard_signature(
trace!("canonical request:\n{}", canonical_request);
trace!("string to sign:\n{}", string_to_sign);
let key = verify_v4(garage, service, &authorization, string_to_sign.as_bytes()).await?;
let key = verify_v4(garage, service, &authorization, string_to_sign.as_bytes())?;
let content_sha256 = if authorization.content_sha256 == UNSIGNED_PAYLOAD {
None
} else if authorization.content_sha256 == STREAMING_AWS4_HMAC_SHA256_PAYLOAD {
let bytes = hex::decode(authorization.signature).ok_or_bad_request("Invalid signature")?;
Some(Hash::try_from(&bytes).ok_or_bad_request("Invalid signature")?)
} else {
let bytes = hex::decode(authorization.content_sha256)
.ok_or_bad_request("Invalid content sha256 hash")?;
Some(Hash::try_from(&bytes).ok_or_bad_request("Invalid content sha256 hash")?)
};
let content_sha256_header = parse_x_amz_content_sha256(Some(&authorization.content_sha256))?;
Ok((Some(key), content_sha256))
Ok(CheckedSignature {
key: Some(key),
content_sha256_header,
signature_header: Some(authorization.signature),
})
}
async fn check_presigned_signature(
fn check_presigned_signature(
garage: &Garage,
service: &'static str,
request: &mut Request<IncomingBody>,
mut query: QueryMap,
) -> Result<(Option<Key>, Option<Hash>), Error> {
) -> Result<CheckedSignature, Error> {
let algorithm = query.get(&X_AMZ_ALGORITHM).unwrap();
let authorization = Authorization::parse_presigned(&algorithm.value, &query)?;
// Verify that all necessary request headers are included in signed_headers
// For AWSv4 pre-signed URLs, the following must be incldued:
// For AWSv4 pre-signed URLs, the following must be included:
// - the Host header (mandatory)
// - all x-amz-* headers used in the request
let signed_headers = split_signed_headers(&authorization)?;
@@ -161,7 +179,7 @@ async fn check_presigned_signature(
trace!("canonical request (presigned url):\n{}", canonical_request);
trace!("string to sign (presigned url):\n{}", string_to_sign);
let key = verify_v4(garage, service, &authorization, string_to_sign.as_bytes()).await?;
let key = verify_v4(garage, service, &authorization, string_to_sign.as_bytes())?;
// In the page on presigned URLs, AWS specifies that if a signed query
// parameter and a signed header of the same name have different values,
@@ -193,7 +211,11 @@ async fn check_presigned_signature(
// Presigned URLs always use UNSIGNED-PAYLOAD,
// so there is no sha256 hash to return.
Ok((Some(key), None))
Ok(CheckedSignature {
key: Some(key),
content_sha256_header: ContentSha256Header::UnsignedPayload,
signature_header: Some(authorization.signature),
})
}
pub fn parse_query_map(uri: &http::uri::Uri) -> Result<QueryMap, Error> {
@@ -306,7 +328,7 @@ pub fn canonical_request(
// Note that there is also the issue of path normalization, which I hope is unrelated to the
// one of URI-encoding. At least in aws-sigv4 both parameters can be set independently,
// and rusoto_signature does not seem to do any effective path normalization, even though
// it mentions it in the comments (same link to the souce code as above).
// it mentions it in the comments (same link to the source code as above).
// We make the explicit choice of NOT normalizing paths in the K2V API because doing so
// would make non-normalized paths invalid K2V partition keys, and we don't want that.
let canonical_uri: std::borrow::Cow<str> = if service != "s3" {
@@ -357,7 +379,7 @@ pub fn parse_date(date: &str) -> Result<DateTime<Utc>, Error> {
Ok(Utc.from_utc_datetime(&date))
}
pub async fn verify_v4(
pub fn verify_v4(
garage: &Garage,
service: &str,
auth: &Authorization,
@@ -370,12 +392,18 @@ pub async fn verify_v4(
let key = garage
.key_table
.get(&EmptyKey, &auth.key_id)
.await?
.get_local(&EmptyKey, &auth.key_id)?
.filter(|k| !k.state.is_deleted())
.ok_or_else(|| Error::forbidden(format!("No such key: {}", &auth.key_id)))?;
let key_p = key.params().unwrap();
if key_p.is_expired(now_msec()) {
return Err(Error::forbidden(format!(
"Access key {} has expired",
key.key_id
)));
}
let mut hmac = signing_hmac(
&auth.date,
&key_p.secret_key,
@@ -442,7 +470,7 @@ impl Authorization {
.to_string();
let content_sha256 = headers
.get(X_AMZ_CONTENT_SH256)
.get(X_AMZ_CONTENT_SHA256)
.ok_or_bad_request("Missing X-Amz-Content-Sha256 field")?;
let date = headers
@@ -518,7 +546,7 @@ impl Authorization {
})
}
pub(crate) fn parse_form(params: &HeaderMap) -> Result<Self, Error> {
pub fn parse_form(params: &HeaderMap) -> Result<Self, Error> {
let algorithm = params
.get(X_AMZ_ALGORITHM)
.ok_or_bad_request("Missing X-Amz-Algorithm header")?

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